# SECURITY_MODEL.md — NexStock Security Architecture

## Overview

NexStock handles sensitive business data (sales figures, stock levels, financial information). Security is not optional — it's a core architectural requirement. This document defines the security model across all layers.

## 1. Authentication

### Registration & Login
- Laravel Breeze / Fortify for authentication scaffolding
- Email + password authentication
- Email verification required for owners
- Password requirements: minimum 8 characters, checked against breached password lists
- Account lockout after 5 failed attempts (15-minute cooldown)
- Rate limiting: 5 login attempts per minute per IP

### Two-Factor Authentication (2FA)
- TOTP-based 2FA (Google Authenticator, Authy, etc.)
- Optional for owners, can be enforced
- Recovery codes generated on setup
- 2FA required for sensitive operations (billing changes, user management)

### Session Management
- Server-side sessions (Redis-backed)
- Session timeout: 8 hours of inactivity
- Single session enforcement optional (configurable)
- Session invalidation on password change
- Secure session cookies (HttpOnly, Secure, SameSite=Lax)

### Platform Admin Authentication
- Completely separate authentication guard
- Separate login URL (`/admin/login`)
- Mandatory 2FA for all platform admins
- IP allowlisting (optional)
- Separate session storage

## 2. Authorization

### Role-Based Access

| Role | Scope | Description |
|------|-------|-------------|
| Owner | Full business access | Can manage everything within their business |
| Employee | Configurable permissions | Limited to granted permissions |
| Platform Admin | Platform-wide | Manages businesses, plans, system health |

### Permission System

Employee permissions are granular and configurable:

```php
enum Permission: string {
    // Sales
    case Sell = 'sell';
    case ViewSales = 'view_sales';
    case ProcessReturn = 'process_return';
    case ApplyDiscount = 'apply_discount';
    
    // Products
    case ViewProducts = 'view_products';
    case ManageProducts = 'manage_products';
    
    // Stock
    case ViewStock = 'view_stock';
    case AdjustStock = 'adjust_stock';
    case RecordStock = 'record_stock';
    
    // Purchases
    case ViewPurchases = 'view_purchases';
    case ManagePurchases = 'manage_purchases';
    case ReceiveStock = 'receive_stock';
    
    // Cash
    case ManageCashShift = 'manage_cash_shift';
    
    // Expenses
    case ViewExpenses = 'view_expenses';
    case ManageExpenses = 'manage_expenses';
    
    // Reports
    case ViewReports = 'view_reports';
    case ExportReports = 'export_reports';
}
```

### Policy Enforcement

All authorization is enforced server-side via Laravel Policies:

```php
// Every controller action uses policies
class ProductController
{
    public function update(UpdateProductRequest $request, Product $product)
    {
        $this->authorize('update', $product); // Policy check
        // ...
    }
}
```

**Rules:**
- Owners automatically pass all business-level policy checks
- Employees must have explicit permissions
- Platform admins use a separate authorization layer
- Frontend visibility is a UX convenience, NOT a security boundary
- Every API endpoint verifies authorization independently

## 3. Tenant Isolation

This is the most critical security layer.

### Principles
1. **Never trust tenant_id from client** — always resolve from authenticated user
2. **Global scopes enforce isolation at the query level** — impossible to accidentally query another tenant's data
3. **Middleware validates tenant context** — requests without valid tenant context are rejected
4. **Database constraints provide last line of defense** — foreign keys prevent orphaned records

### Implementation

```php
// Tenant context is resolved from the authenticated user's business
class TenantContext
{
    public function id(): string
    {
        return auth()->user()->tenant_id;
    }
    
    public function business(): Business
    {
        return auth()->user()->business;
    }
}

// Global scope automatically applied to all tenant models
class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where($model->getTable() . '.tenant_id', app(TenantContext::class)->id());
    }
}

// Trait for tenant-owned models
trait BelongsToTenant
{
    public static function bootBelongsToTenant(): void
    {
        static::addGlobalScope(new TenantScope());
        
        static::creating(function (Model $model) {
            if (!$model->tenant_id) {
                $model->tenant_id = app(TenantContext::class)->id();
            }
        });
    }
}
```

### Isolation Coverage

| Layer | Mechanism |
|-------|-----------|
| Database queries | Global scope with `tenant_id` |
| Model creation | Auto-populate `tenant_id` on creating |
| Route model binding | Scoped to tenant |
| File storage | Tenant-prefixed paths |
| Cache | Tenant-prefixed keys |
| Queue jobs | Tenant context serialized with job |
| Broadcast channels | Tenant membership verified |
| Notifications | Scoped to tenant users |
| Exports | Filtered to tenant data |

### Cross-Tenant Testing

```php
// Automated test: verify cross-tenant access is denied
it('prevents accessing another tenant product', function () {
    $tenant1 = Business::factory()->create();
    $tenant2 = Business::factory()->create();
    
    $product = Product::factory()->for($tenant1)->create();
    
    actingAs(User::factory()->for($tenant2)->create());
    
    get(route('products.show', $product))
        ->assertForbidden(); // or assertNotFound
});
```

## 4. Input Validation

### Form Requests
Every endpoint that accepts user input uses a Form Request:

```php
class CreateProductRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'sku' => ['nullable', 'string', 'max:100', 
                Rule::unique('product_variants', 'sku')
                    ->where('tenant_id', $this->user()->tenant_id)],
            'barcode' => ['nullable', 'string', 'max:100',
                Rule::unique('product_variants', 'barcode')
                    ->where('tenant_id', $this->user()->tenant_id)],
            'selling_price' => ['required', 'numeric', 'min:0', 'max:999999999'],
            'purchase_cost' => ['required', 'numeric', 'min:0', 'max:999999999'],
            'category_id' => ['required', 'uuid', 'exists:categories,id'],
            // ...
        ];
    }
}
```

### Validation Rules
- All user input is validated server-side
- Numeric inputs have min/max bounds
- String inputs have max length
- UUIDs are validated as valid UUIDs
- Existence checks are tenant-scoped
- Uniqueness checks are tenant-scoped
- File uploads validated for type, size, and content

## 5. CSRF & XSS Protection

- **CSRF**: Laravel's built-in CSRF protection via `VerifyCsrfToken` middleware
- **XSS**: Inertia.js automatically escapes rendered data
- **Content-Security-Policy**: Restrictive CSP headers in production
- **X-Frame-Options**: DENY (prevent clickjacking)
- **X-Content-Type-Options**: nosniff

## 6. Rate Limiting

| Endpoint | Limit | Window |
|----------|-------|--------|
| Login | 5 attempts | per minute |
| Registration | 3 attempts | per minute |
| Password reset | 3 attempts | per minute |
| API endpoints | 60 requests | per minute |
| Export | 5 requests | per minute |
| Import | 3 requests | per minute |
| POS (sale completion) | 30 requests | per minute |

## 7. File Security

### Upload Validation
```php
'image' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'], // 2MB max
'import_file' => ['required', 'file', 'mimes:csv,xlsx', 'max:10240'], // 10MB max
```

### Storage Rules
- All files stored in S3-compatible storage
- **Private by default** — no public URLs
- Access via signed URLs with expiry (15 minutes)
- Tenant-scoped paths: `tenants/{tenant_id}/products/{filename}`
- Virus scanning on upload (if infrastructure supports)
- No executable file types accepted

## 8. Data Protection

### Encryption
- Passwords: bcrypt (via Laravel's Hash facade)
- 2FA secrets: encrypted at rest (AES-256-CBC via Laravel's Crypt)
- Sensitive business data: encrypted where required
- Database connections: TLS in production
- All traffic: HTTPS only in production

### PII Handling
- Minimize PII collection — only collect what's needed
- Customer data exportable (data portability)
- Customer data deletable on request
- Audit log records who accessed PII

## 9. Audit Trail

### Audited Actions

| Category | Events |
|----------|--------|
| Authentication | Login, logout, failed login, password change, 2FA setup |
| Users | Create, update, deactivate, permission change |
| Products | Create, update, archive, price change |
| Inventory | Adjustment, damage, loss, receiving, reconciliation |
| Sales | Complete, void, return, refund |
| Purchases | Create, receive, cancel |
| Cash | Shift open/close, cash in/out |
| Settings | Business settings change, tax change |
| Billing | Plan change, subscription event |
| Admin | Support access, suspension, reactivation |

### Audit Record Structure
```php
[
    'action' => 'product.price_changed',
    'user_id' => 'uuid',
    'tenant_id' => 'uuid',
    'auditable_type' => 'ProductVariant',
    'auditable_id' => 'uuid',
    'old_values' => ['selling_price' => 5000],
    'new_values' => ['selling_price' => 5500],
    'ip_address' => '192.168.1.1',
    'metadata' => ['reason' => 'Price adjustment'],
    'created_at' => '2026-08-29T12:00:00Z',
]
```

## 10. API Security

### Idempotency
- Sale completion accepts an `idempotency_key`
- Duplicate requests with the same key return the original result
- Keys are unique per tenant
- Keys expire after 24 hours

### SQL Injection Prevention
- All queries use Eloquent ORM or parameterized queries
- No raw SQL with user input
- DB::raw() used only with constant values

### Mass Assignment Protection
- All models define `$fillable` or `$guarded`
- Form Requests validate all input before it reaches the model
- No `$guarded = []` on any model

## 11. Infrastructure Security

### Production Requirements
- HTTPS only (HSTS enabled)
- Database credentials via environment variables
- Application key rotated periodically
- Debug mode OFF in production
- Error pages don't expose stack traces
- Logs don't contain sensitive data (passwords, tokens)
- Redis password-protected
- MySQL user with minimal privileges
- Regular backups with encryption at rest

### Docker Security
- Non-root user in containers
- Read-only filesystem where possible
- Minimal base images
- No unnecessary ports exposed
- Secrets via environment variables, not build args

## 12. Dependency Security

- Regular `composer audit` checks
- Regular `npm audit` checks
- Dependabot or similar for automated dependency updates
- Pin major versions to prevent breaking changes
- Review changelogs before major updates
