# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

**2AB Portal** - User management and company portal built with Symfony 6.4, featuring multi-layered security, company-scoped access control, and a modern UI with Tailwind CSS + Flowbite.

**Tech Stack:**
- Backend: Symfony 6.4 + Doctrine ORM + PHP 8.1+
- Frontend: Tailwind CSS 3.4 + Flowbite 4.0 + Hotwired (Turbo + Stimulus)
- Database: MariaDB (MySQL compatible)
- Build: Webpack Encore 5.1

## Common Development Commands

### Development Workflow
```bash
composer install && npm install
symfony serve                        # Start dev server
npm run watch                        # Auto-compile assets (separate terminal)
npm run build                        # Production build
npm run build:all                    # Build + clear Symfony cache
```

### Database Operations
```bash
# DEVELOPMENT: Quick schema update (bypasses migrations)
php bin/console doctrine:schema:update --force

# PRODUCTION: Create and run migrations
php bin/console make:migration
php bin/console doctrine:migrations:migrate
```

### Cache & Validation
```bash
php bin/console cache:clear
php bin/console lint:container       # Validate DI container
php bin/console lint:twig templates/ # Validate Twig templates
php bin/console lint:yaml config/    # Validate YAML config
```

### Testing
```bash
php bin/phpunit
php bin/phpunit tests/Service/Auth/AuthenticationServiceTest.php
```

## Architecture

### Role Hierarchy & Company Isolation

**3 roles only:**
```
ROLE_ADMIN > ROLE_MANAGER > ROLE_EMPLOYER > ROLE_USER
```

**Company Isolation Rules:**
- **ROLE_ADMIN**: Full system access, creates companies, assigns managers
- **ROLE_MANAGER**: Scoped to their company only. Can view company, manage employees (ROLE_EMPLOYER), manage documents/categories. Cannot create/edit/delete companies.
- **ROLE_EMPLOYER**: Scoped to their company, view-only. Blocked from portal if not assigned to a company (redirected to `/no-company`).

**Enforcement layers:**
1. `security.yaml` access_control rules (route-level)
2. `#[IsGranted('ROLE_X')]` attributes on controllers (class/method-level)
3. `CompanyVoter` with `COMPANY_VIEW`, `COMPANY_EDIT`, `COMPANY_MANAGE_EMPLOYEES` (resource-level)
4. `CompanyAccessListener` blocks ROLE_EMPLOYER without company on every request

**Security Configuration (`config/packages/security.yaml`):**
- Two firewalls: `api` (stateless JWT) + `main` (form login with remember-me)
- Role hierarchy: `ROLE_ADMIN` > `ROLE_MANAGER` > `ROLE_EMPLOYER` > `ROLE_USER`

### Company Voter (`src/Security/Voter/CompanyVoter.php`)

```php
// Usage in controllers:
$this->denyAccessUnlessGranted('COMPANY_VIEW', $company);           // Read access
$this->denyAccessUnlessGranted('COMPANY_EDIT', $company);           // Edit company
$this->denyAccessUnlessGranted('COMPANY_MANAGE_EMPLOYEES', $company); // Manage employees/docs
```

- `ROLE_ADMIN` → always granted
- `ROLE_MANAGER` → granted if `user->hasCompany($company)` (VIEW + EDIT + MANAGE)
- `ROLE_EMPLOYER` → granted for VIEW only if member of company

### Unified Layout System

All authenticated pages use `templates/layouts/authenticated.html.twig`.

```
base.html.twig → flowbite_base.html.twig → authenticated.html.twig
```

**Template pattern:**
```twig
{% extends 'layouts/authenticated.html.twig' %}
{% block page_title %}Page Title{% endblock %}
{% block content %}{# content #}{% endblock %}
```

**Role Detection:** `active_role()` Twig function detects user's role and adjusts sidebar menu. Never create role-specific layouts.

### Media Permissions

All media access is company-scoped:
- `VISIBILITY_PRIVATE` = accessible to members of the same company (not all authenticated users)
- `VISIBILITY_PROTECTED` = explicit `MediaAccess` grants required
- `Media::getCompany()` resolves company via `relatedCompany`, `documentCategory.company`, or uploader's primary company
- Admin sees all media; Manager sees only their company's media; Employer sees only their company's media

### Entity Relationships

**User-Company (Many-to-Many via UserCompany):**
- `User` ↔ `UserCompany` ↔ `Company`
- `UserCompany` has: `role` (owner/admin/manager/employee/member), `joinedAt`, `leftAt`, `isActive`
- `User.getPrimaryCompany()` returns first active company

**User Management:**
- `User` ↔ `UserDevice` (one-to-many)
- `User` ↔ `UserActivity` (one-to-many)
- `User` ↔ `JwtToken` (one-to-many)
- `User` ↔ `RefreshToken` (one-to-many)
- `User` ↔ `LoginAttempt` (one-to-many)

**Media:**
- `Media` → `User` (uploadedBy), `User` (relatedUser), `Company` (relatedCompany)
- `Media` → `DocumentCategory` → `Company`
- `Media` ↔ `MediaAccess` (one-to-many), `MediaAccessLog`, `MediaTag`, `DocumentReminder`

### Entities (35 total)

**Core:** User, Company, UserCompany, ActivitySector, Media, MediaAccess, MediaAccessLog, MediaTag, DocumentCategory, DocumentReminder, Tag, NotificationApp

**Security/Audit (append-only, never deleted):** UserActivity, UserDevice, UserSession, UserApprovedCountry, LoginAttempt, HttpRequestLog, IpBlock, IpValidationCode, JwtToken, RefreshToken, PasswordResetToken, TwoFactorCode, DeviceApprovalCode, DeviceApp, DeviceDiagnostic, EmailLog

**SMS:** SmsConfiguration, SmsCampaign, SmsCampaignLine, SmsLog

**Other:** SmtpConfiguration, AppCrash, AppLog

### Gedmo Soft-Delete (MANDATORY pattern)

```php
#[ORM\Entity(repositoryClass: MyEntityRepository::class)]
#[ORM\Table(name: 'my_entity')]
#[ORM\Index(name: 'idx_my_entity_deleted', columns: ['deleted_at'])]  // REQUIRED
#[Gedmo\SoftDeleteable(fieldName: 'deletedAt', timeAware: false)]
class MyEntity
{
    use TimestampableEntity;      // createdAt, updatedAt
    use SoftDeleteableEntity;     // deletedAt
}
```

**Entities with soft-delete:** User, ActivitySector, Company, Media, NotificationApp, DocumentReminder, MediaTag, Tag

**Never add soft-delete to:** audit/security entities (UserActivity, LoginAttempt, HttpRequestLog, etc.)

### Security Architecture (7 Layers)

1. **IP Blocking** (`IpBlockingService`) - Threat scoring, rate limiting, proxy detection
2. **Login Attempt Tracking** (`LoginAttemptService`) - 7 attempts / 30 min = 15 min lock
3. **JWT Authentication** (`JwtTokenService`) - Stateless API auth with token rotation
4. **Two-Factor Authentication** (`TwoFactorService`) - TOTP codes via email
5. **Device Approval** (`DeviceApprovalService`) - New device detection, email codes
6. **Token Security** (`TokenSecurityService`) - Location/user-agent validation
7. **Account Locking** (`SecurityAlertService`) - Auto-lock on breach threshold

### Event Listeners

- `CompanyAccessListener` - **Blocks ROLE_EMPLOYER without company** (priority 5)
- `AuthenticationFailureListener` - Log failed login attempts
- `IpBlockingRequestListener` - Enforce IP blocks pre-authentication
- `HttpLoggingSubscriber` - Log all HTTP requests
- `TokenSecurityListener` - Validate JWT tokens
- `LoginSuccessListener` - Track successful logins
- `JWTCreatedListener` - Enrich JWT payload
- `ForcePasswordChangeListener` - Redirect users requiring password change
- `ExpiredTokenReuseListener` - Detect token replay attacks
- `AppVersionListener` - Track app version changes
- `TwoFactorListener` / `TwoFactorRequestListener` - 2FA flow
- `IpValidationListener` - IP validation flow

### Service Layer

Services in `src/Service/`:
- **Auth/** - Authentication, JWT, 2FA, device management, password reset, role management
- **Security/** - IP blocking, token security, geolocation
- **Email/** - Email sending, SMTP configuration
- **Logging/** - FileLogger, HTTP request logging
- **System/** - Database backup, cache management
- **Database/** - EntityManagerService (safe DB operations)
- **Media/** - MediaService, MediaAccessService, ChunkedUploadService, ZipArchiveService
- **Navigation/** - NavigationService (breadcrumb contexts)
- **Notification/** - NotificationService
- **Messenger/** - Async message config
- **Sms/** - SMS sending

### Messenger (Async Processing)

Controlled via `MESSENGER_MODE` env var (`sync` for dev, `async` for prod).

Messages: `SendEmailMessage`, `LogHttpRequestMessage`, `BlockIpMessage`, `SendSecurityNotificationMessage`

## Key Development Patterns

### Controller Checklist

Every controller method performing I/O MUST:
- [ ] Have try-catch block
- [ ] Log errors: action, user_id, error, file, line
- [ ] Use `EntityManagerService` safe methods (never direct EntityManager)
- [ ] Show user-friendly flash message on error
- [ ] Have company voter check if accessing company-scoped resources

### Company-Scoped Controller Pattern

```php
#[Route('/companies/{id}/something')]
#[IsGranted('ROLE_EMPLOYER')]
public function something(Company $company): Response
{
    $this->denyAccessUnlessGranted('COMPANY_VIEW', $company);  // or COMPANY_MANAGE_EMPLOYEES
    // ... business logic
}
```

### Manager Controller Pattern (auto-scoping)

```php
// In ManagerUserController - always scope to manager's company
$company = $user->getPrimaryCompany();
if (!$company) {
    return $this->redirectToRoute('app_no_company');
}
$query = $this->userRepository->findAllQueryForManagerByCompany($company, $filters);
```

### Template Role Checks

```twig
{# Show write buttons only to admin/manager #}
{% if is_granted('ROLE_ADMIN') or is_granted('ROLE_MANAGER') %}
    <button>Modifier</button>
{% endif %}

{# Admin-only actions #}
{% if is_granted('ROLE_ADMIN') %}
    <button>Supprimer</button>
{% endif %}
```

### Error Handling

```php
use App\Service\Logging\FileLogger;
use App\Service\Database\EntityManagerService;

try {
    $this->entityManager->safePersist($entity);
    $this->fileLogger->info(['action' => 'created', 'user_id' => $user->getId()]);
    $this->addFlash('success', 'Opération réussie.');
} catch (\Exception $e) {
    $this->fileLogger->error([
        'action' => 'create_failed', 'user_id' => $user->getId(),
        'error' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine()
    ], true);  // true = send email notification
    $this->addFlash('error', 'Une erreur est survenue.');
}
```

### Flash Messages

```php
$this->addFlash('success', 'Opération réussie.');
$this->addFlash('error', 'Une erreur est survenue.');
$this->addFlash('warning', 'Attention.');
$this->addFlash('info', 'Information.');
```

Auto-displayed as toasts via `_notifications_zone.html.twig`.

### Flowbite Macros

```twig
{% import 'flowbite_components/macros.html.twig' as flowbite %}
{{ flowbite.alert('success', 'Done!', true) }}
{{ flowbite.button('Save', 'blue', 'md', 'save') }}
{{ flowbite.badge('Active', 'green') }}
{{ flowbite.input('email', 'Email', 'email', '', 'user@example.com', true) }}
```

## Critical Environment Variables

```env
SESSION_COOKIE_LIFETIME=604800       # 7 days
REMEMBER_ME_LIFETIME=1209600         # 14 days
ENABLE_AUTO_IP_BLOCKING=false
ENABLE_AUTO_ACCOUNT_LOCK=false
RATE_LIMIT_MAX_ATTEMPTS=7
RATE_LIMIT_LOCK_DURATION=15          # minutes
MESSENGER_MODE=sync                  # sync or async
EMAIL_SEND_IMMEDIATE=true
SUPPORT_EMAIL=devyayaly@gmail.com
```

## Troubleshooting

```bash
# Assets not compiling
rm -rf node_modules package-lock.json && npm install && npm run build

# Cache issues
php bin/console cache:clear

# JWT not working
php bin/console lexik:jwt:generate-keypair --overwrite

# DB connection
php bin/console doctrine:query:sql "SELECT 1"

# Schema validation
php bin/console doctrine:schema:validate
```

---

**Version:** 3.0
**Last Updated:** March 2026
**Changes:** Removed all business modules (GesIntrant, GesContrat, GesFinance, GesProject, GesVoiture). Simplified to 3 roles (Admin/Manager/Employer). Added company isolation with CompanyVoter and CompanyAccessListener. Media permissions now company-scoped.
