Initial commit

This commit is contained in:
2026-03-24 17:01:12 +00:00
commit a88504357d
237 changed files with 34375 additions and 0 deletions

34
.dockerignore Normal file
View File

@@ -0,0 +1,34 @@
# Dependencies
src/node_modules
src/vendor
# Git
.git
.gitignore
# IDE
.idea
.vscode
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Build artifacts
src/public/build
src/public/hot
# Testing
src/coverage
# Logs
*.log
src/storage/logs/*
# Cache
src/bootstrap/cache/*
src/storage/framework/cache/*
src/storage/framework/sessions/*
src/storage/framework/views/*

29
.env.example Normal file
View File

@@ -0,0 +1,29 @@
# Docker Compose Configuration
APP_PORT=8080
REDIS_PORT=6379
MAIL_PORT=1025
MAIL_DASHBOARD_PORT=8025
# ============================================
# DATABASE CONFIGURATION
# ============================================
# Choose one: mysql, pgsql, sqlite
# Start Docker with: docker-compose --profile mysql up -d
# docker-compose --profile pgsql up -d
# docker-compose --profile sqlite up -d
# Common settings
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret
# MySQL specific
DB_PORT=3306
DB_ROOT_PASSWORD=rootsecret
# PostgreSQL specific (uses DB_PORT=5432 by default)
# Uncomment if using PostgreSQL:
# DB_PORT=5432
# SQLite specific
# No additional settings needed - uses database/database.sqlite

145
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,145 @@
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
tests:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: laravel_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
redis:
image: redis:alpine
ports:
- 6379:6379
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mbstring, dom, fileinfo, mysql, redis
coverage: pcov
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install Composer dependencies
working-directory: ./src
run: composer install --no-progress --prefer-dist --optimize-autoloader
- name: Copy .env
working-directory: ./src
run: |
cp .env.example .env
php artisan key:generate
- name: Configure test environment
working-directory: ./src
run: |
echo "DB_CONNECTION=mysql" >> .env
echo "DB_HOST=127.0.0.1" >> .env
echo "DB_PORT=3306" >> .env
echo "DB_DATABASE=laravel_test" >> .env
echo "DB_USERNAME=root" >> .env
echo "DB_PASSWORD=password" >> .env
echo "REDIS_HOST=127.0.0.1" >> .env
echo "CACHE_DRIVER=redis" >> .env
echo "QUEUE_CONNECTION=redis" >> .env
- name: Run migrations
working-directory: ./src
run: php artisan migrate --force
- name: Run tests
working-directory: ./src
run: php artisan test --parallel
- name: Check code style
working-directory: ./src
run: ./vendor/bin/pint --test
deploy-staging:
needs: tests
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to staging
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
script: |
cd /var/www/staging
git pull origin develop
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart
deploy-production:
needs: tests
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to production
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
script: |
cd /var/www/production
php artisan down
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart
php artisan up

36
.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# Environment files
.env
!.env.example
# Docker volumes
mysql_data/
redis_data/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Laravel - commit most files, ignore generated/local files
src/.env
src/vendor/
src/node_modules/
src/bootstrap/cache/*
!src/bootstrap/cache/.gitignore
src/storage/*.key
src/storage/logs/*
!src/storage/logs/.gitkeep
src/storage/framework/cache/*
!src/storage/framework/cache/.gitignore
src/storage/framework/sessions/*
!src/storage/framework/sessions/.gitignore
src/storage/framework/views/*
!src/storage/framework/views/.gitignore
src/public/hot
src/public/storage
src/public/build

View File

362
AI_CONTEXT.md Normal file
View File

@@ -0,0 +1,362 @@
# AI Assistant Context
> **🚨 CRITICAL: For detailed module development instructions, see [CLAUDE.md](CLAUDE.md)**
> That document contains EXACT file templates, naming conventions, and step-by-step instructions.
This document provides context for AI coding assistants working on projects built with this template.
## Development Workflow Rules
### Git Branching Strategy
**Rule**: The AI agent must create a new Git branch for every new feature, module, or significant change. It should never commit or push directly to the `main` branch.
**Rationale**: This practice is crucial for maintaining a clean and stable main branch, facilitating code reviews, and making it easier for human developers to collaborate and manage the project's history.
**Example Workflow**:
1. `git checkout -b feature/new-company-module`
2. *...perform all work related to the new module...*
3. `git add .`
4. `git commit -m "feat: Create new company module"`
5. *...agent informs the user that the feature is complete on the new branch...*
## Template Overview
This is a **ready-to-use Laravel Docker Development Template** with everything pre-installed:
- ✅ Laravel 11 with Breeze authentication (Blade + dark mode)
- ✅ Filament v3.3 admin panel with user management
- ✅ Pest testing framework
- ✅ Laravel Pint code style
- ✅ Docker-based development environment
- ✅ Production deployment to Ubuntu 24.04 (no Docker)
- ✅ Modular architecture
- ✅ Multi-database support (MySQL, PostgreSQL, SQLite)
**Setup time:** 2 minutes - just run `./setup.sh` and everything is ready!
## Technology Stack
| Layer | Technology |
|-------|------------|
| **Backend** | Laravel 11+, PHP 8.2+ |
| **Frontend** | Blade, Livewire (NO Vue/React/Inertia) |
| **Admin** | Filament 3.x |
| **Database** | MySQL 8 / PostgreSQL 15 / SQLite |
| **Cache/Queue** | Redis |
| **Auth** | Laravel Breeze (Blade + Livewire) - PRE-INSTALLED |
| **Testing** | Pest - PRE-INSTALLED |
| **Permissions** | spatie/laravel-permission - PRE-INSTALLED |
| **Audit** | owen-it/laravel-auditing - PRE-INSTALLED |
| **Error Tracking** | spatie/laravel-flare + spatie/laravel-ignition - PRE-INSTALLED |
| **API Auth** | Laravel Sanctum - PRE-INSTALLED |
| **Code Style** | Laravel Pint |
## 🚨 CRITICAL: Debugging Strategy
**ALWAYS CHECK LOGS FIRST - NEVER GUESS AT SOLUTIONS**
When encountering errors:
### Step 1: Check Laravel Logs
```bash
docker-compose exec app cat storage/logs/laravel.log | grep -A 20 "Error"
```
### Step 2: Identify Root Cause
- Read the full stack trace
- Find the exact file and line number
- Understand what the code is trying to do
### Step 3: Fix and Verify
- Make targeted fix to root cause
- Clear relevant caches
- Test the specific scenario
### Common Commands:
```bash
# View recent errors
docker-compose exec app tail -n 100 storage/logs/laravel.log
# Check container logs
docker-compose logs --tail=50 app
docker-compose logs --tail=50 nginx
# Clear caches after fixes
docker-compose exec app php artisan optimize:clear
docker-compose exec app php artisan permission:cache-reset
```
**See [DEBUGGING.md](DEBUGGING.md) for complete debugging guide.**
## Important: No JavaScript Frameworks
**This template deliberately avoids JavaScript frameworks** (Vue, React, Inertia) to keep debugging simple. All frontend is:
- Blade templates
- Livewire for reactivity
- Alpine.js (included with Livewire)
- Tailwind CSS
Do NOT suggest or implement Vue/React/Inertia solutions.
## Architecture
### Module Structure
Features are organized as **modules** in `app/Modules/`:
```
app/Modules/
└── ModuleName/
├── Config/
│ └── module_name.php # Module config (incl. audit settings)
├── Database/
│ ├── Migrations/
│ └── Seeders/
├── Filament/
│ └── Resources/ # Admin CRUD + Audit Log
├── Http/
│ ├── Controllers/
│ ├── Middleware/
│ └── Requests/
├── Models/ # With ModuleAuditable trait
├── Policies/
├── Services/ # Business logic goes here
├── Routes/
│ ├── web.php
│ └── api.php
├── Resources/
│ ├── views/
│ ├── css/
│ └── lang/
├── Permissions.php # Module permissions
└── ModuleNameServiceProvider.php
```
### Creating Modules
```bash
# Basic module
php artisan make:module ModuleName
# With model + Filament resource + audit
php artisan make:module ModuleName --model=ModelName
# With API routes
php artisan make:module ModuleName --model=ModelName --api
```
### Key Traits and Interfaces
**For auditable models:**
```php
use App\Traits\ModuleAuditable;
use OwenIt\Auditing\Contracts\Auditable;
class YourModel extends Model implements Auditable
{
use ModuleAuditable;
// Exclude sensitive fields from audit
protected $auditExclude = ['password', 'secret_key'];
}
```
## File Locations
| What | Where |
|------|-------|
| Laravel app | `src/` |
| Docker configs | `docker/` |
| Production deploy | `deploy/` |
| Setup scripts | `scripts/` |
| Documentation | `docs/` |
| Module code | `src/app/Modules/` |
| Filament resources | `src/app/Modules/*/Filament/Resources/` |
| Module views | `src/app/Modules/*/Resources/views/` |
| Module routes | `src/app/Modules/*/Routes/` |
| Module migrations | `src/app/Modules/*/Database/Migrations/` |
## Common Tasks
### Add a New Feature
1. Create module: `php artisan make:module FeatureName --model=MainModel`
2. Edit migration: `app/Modules/FeatureName/Database/Migrations/`
3. Update model fillables: `app/Modules/FeatureName/Models/`
4. Customize Filament resource: `app/Modules/FeatureName/Filament/Resources/`
5. Run migrations: `php artisan migrate`
6. Seed permissions: `php artisan db:seed --class=PermissionSeeder`
### Add a Model to Existing Module
```bash
# Create model manually
php artisan make:model Modules/ModuleName/Models/NewModel -m
# Move migration to module folder
mv database/migrations/*_create_new_models_table.php \
app/Modules/ModuleName/Database/Migrations/
# Add audit trait to model
# Create Filament resource manually or use:
php artisan make:filament-resource NewModel \
--model=App\\Modules\\ModuleName\\Models\\NewModel
```
### Add API Endpoint
```php
// app/Modules/ModuleName/Routes/api.php
Route::prefix('api/module-name')
->middleware(['api', 'auth:sanctum'])
->group(function () {
Route::get('/items', [ItemController::class, 'index']);
Route::post('/items', [ItemController::class, 'store']);
});
```
### Add Permission
```php
// app/Modules/ModuleName/Permissions.php
return [
'module_name.view' => 'View Module',
'module_name.create' => 'Create records',
'module_name.edit' => 'Edit records',
'module_name.delete' => 'Delete records',
'module_name.new_action' => 'New custom action', // Add this
];
```
Then run: `php artisan db:seed --class=PermissionSeeder`
### Customize Filament Resource
```php
// app/Modules/ModuleName/Filament/Resources/ModelResource.php
public static function form(Form $form): Form
{
return $form->schema([
Forms\Components\TextInput::make('name')->required(),
Forms\Components\Select::make('status')
->options(['active' => 'Active', 'inactive' => 'Inactive']),
// Add more fields
]);
}
public static function table(Table $table): Table
{
return $table->columns([
Tables\Columns\TextColumn::make('name')->searchable(),
Tables\Columns\BadgeColumn::make('status'),
// Add more columns
]);
}
```
## Environment
### Development
- Run commands via `make shell` then `php artisan ...`
- Or use `make artisan cmd='...'`
- Database: accessible at `localhost:3306` (MySQL) or `localhost:5432` (PostgreSQL)
- Mail: caught by Mailpit at `localhost:8025`
### Production
- No Docker - native PHP on Ubuntu 24.04
- Web server: Nginx or Apache
- Use `deploy/scripts/server-setup.sh` for initial setup
- Use `deploy/scripts/deploy.sh` for deployments
## Code Style
- **Follow Laravel conventions**
- **Use Pint for formatting**: `make lint`
- **Business logic in Services**, not Controllers
- **Use Form Requests** for validation
- **Use Policies** for authorization
- **Use Resources** for API responses
## Testing
```bash
# Run tests
make test
# With coverage
make test-coverage
# Create test
php artisan make:test Modules/ModuleName/FeatureTest
```
## Debugging
1. **Check Laravel logs**: `storage/logs/laravel.log`
2. **Check container logs**: `make logs`
3. **Use Telescope** (if installed): `/telescope`
4. **Use Tinker**: `make tinker`
5. **Ignition** shows errors in browser (dev only)
## Gotchas
1. **Module views use namespace**: `view('module-slug::viewname')`
2. **Module routes are prefixed**: `/module-slug/...`
3. **Permissions use snake_case**: `module_name.action`
4. **Filament resources auto-discover** from `Filament/Resources/`
5. **Migrations auto-load** from `Database/Migrations/`
6. **Always run PermissionSeeder** after adding permissions
## Quick Reference
### Artisan Commands
```bash
php artisan make:module Name # Create module
php artisan make:module Name --model=M # With model
php artisan make:filament-resource Name # Filament resource
php artisan make:model Name -m # Model + migration
php artisan make:controller Name # Controller
php artisan make:request Name # Form request
php artisan make:policy Name # Policy
php artisan migrate # Run migrations
php artisan db:seed # Run seeders
php artisan config:clear # Clear config cache
php artisan route:list # List routes
```
### Make Commands
```bash
make up # Start containers
make down # Stop containers
make shell # Shell into app
make artisan cmd='...' # Run artisan
make composer cmd='...' # Run composer
make test # Run tests
make lint # Fix code style
make fresh # Reset database
make logs # View logs
make queue-start # Start queue worker
make queue-logs # View queue logs
make scheduler-start # Start scheduler
make backup # Backup database
make restore file=... # Restore database
```
## Documentation Links
- **[CLAUDE.md](CLAUDE.md)** - 🚨 **AI AGENTS: START HERE** - Exact module templates
- [GETTING_STARTED.md](GETTING_STARTED.md) - Setup walkthrough
- [README.md](README.md) - Overview
- [docs/modules.md](docs/modules.md) - Module system
- [docs/audit-trail.md](docs/audit-trail.md) - Audit configuration
- [docs/filament-admin.md](docs/filament-admin.md) - Admin panel
- [docs/laravel-setup.md](docs/laravel-setup.md) - Laravel setup options
- [docs/error-logging.md](docs/error-logging.md) - Error tracking
- [docs/queues.md](docs/queues.md) - Background jobs
- [docs/ci-cd.md](docs/ci-cd.md) - GitHub Actions
- [docs/backup.md](docs/backup.md) - Database backup

782
CLAUDE.md Normal file
View File

@@ -0,0 +1,782 @@
# AI Agent Instructions for Laravel Module Development
> **CRITICAL**: This document provides EXACT specifications for building modules in this Laravel template.
> Follow these instructions PRECISELY. Do NOT deviate, guess, or improvise.
> When in doubt, READ THE EXISTING CODE before making changes.
---
## 🚨 BEFORE YOU START - MANDATORY STEPS
### 1. Always Check Existing Patterns First
```bash
# List existing modules to see patterns
ls -la src/app/Modules/
# Read an existing module's structure
find src/app/Modules/[ExistingModule]/ -type f -name "*.php"
```
### 2. Always Check Logs After Errors
```bash
docker-compose exec app tail -n 100 storage/logs/laravel.log
```
### 3. Never Guess at Solutions
- If you encounter an error, READ the full stack trace
- Find the EXACT file and line number causing the issue
- Understand what the code is trying to do before fixing
---
## 📁 PROJECT STRUCTURE - EXACT PATHS
```
Laravel-Docker-Dev-Template/
├── src/ # ← ALL Laravel code goes here
│ ├── app/
│ │ ├── Filament/
│ │ │ ├── Pages/
│ │ │ │ └── Settings.php # Site settings page
│ │ │ └── Resources/ # Core Filament resources
│ │ │ ├── UserResource.php
│ │ │ ├── RoleResource.php
│ │ │ └── PermissionResource.php
│ │ ├── Http/
│ │ │ ├── Controllers/
│ │ │ └── Middleware/
│ │ ├── Models/
│ │ │ ├── User.php
│ │ │ └── Setting.php
│ │ ├── Modules/ # ← ALL modules go here
│ │ │ └── [ModuleName]/ # ← Each module is a folder
│ │ ├── Providers/
│ │ │ └── AppServiceProvider.php
│ │ └── Traits/
│ │ └── ModuleAuditable.php
│ ├── bootstrap/
│ │ └── app.php # Middleware registration
│ ├── config/
│ ├── database/
│ │ ├── migrations/
│ │ └── seeders/
│ │ ├── DatabaseSeeder.php
│ │ └── RolePermissionSeeder.php
│ ├── resources/
│ │ └── views/
│ │ ├── layouts/
│ │ │ ├── app.blade.php # Authenticated layout
│ │ │ └── guest.blade.php # Guest layout
│ │ └── components/
│ └── routes/
│ ├── web.php
│ ├── api.php
│ └── auth.php
├── docker-compose.yml
├── setup.bat # Windows setup
└── setup.sh # Linux/Mac setup
```
---
## 🔧 MODULE STRUCTURE - EXACT SPECIFICATION
When creating a module named `[ModuleName]` (e.g., `StockManagement`):
```
src/app/Modules/[ModuleName]/
├── Config/
│ └── [module_name].php # snake_case filename
├── Database/
│ ├── Migrations/
│ │ └── YYYY_MM_DD_HHMMSS_create_[table_name]_table.php
│ └── Seeders/
│ └── [ModuleName]Seeder.php
├── Filament/
│ └── Resources/
│ ├── [ModelName]Resource.php
│ └── [ModelName]Resource/
│ └── Pages/
│ ├── List[ModelName]s.php
│ ├── Create[ModelName].php
│ └── Edit[ModelName].php
├── Http/
│ ├── Controllers/
│ │ └── [ModuleName]Controller.php
│ ├── Middleware/
│ └── Requests/
│ └── [ModelName]Request.php
├── Models/
│ └── [ModelName].php
├── Policies/
│ └── [ModelName]Policy.php
├── Services/
│ └── [ModelName]Service.php
├── Routes/
│ ├── web.php
│ └── api.php
├── Resources/
│ └── views/
│ ├── index.blade.php
│ └── layouts/
│ └── module.blade.php
├── Permissions.php
└── [ModuleName]ServiceProvider.php
```
---
## 📝 EXACT FILE TEMPLATES
### 1. Service Provider (REQUIRED)
**File**: `src/app/Modules/[ModuleName]/[ModuleName]ServiceProvider.php`
```php
<?php
namespace App\Modules\[ModuleName];
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Route;
class [ModuleName]ServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->mergeConfigFrom(
__DIR__ . '/Config/[module_name].php',
'[module_name]'
);
}
public function boot(): void
{
$this->loadMigrationsFrom(__DIR__ . '/Database/Migrations');
$this->loadViewsFrom(__DIR__ . '/Resources/views', '[module-slug]');
$this->registerRoutes();
}
protected function registerRoutes(): void
{
Route::middleware(['web', 'auth'])
->prefix('[module-slug]')
->name('[module-slug].')
->group(__DIR__ . '/Routes/web.php');
if (file_exists(__DIR__ . '/Routes/api.php')) {
Route::middleware(['api', 'auth:sanctum'])
->prefix('api/[module-slug]')
->name('api.[module-slug].')
->group(__DIR__ . '/Routes/api.php');
}
}
}
```
**NAMING RULES**:
- `[ModuleName]` = PascalCase (e.g., `StockManagement`)
- `[module_name]` = snake_case (e.g., `stock_management`)
- `[module-slug]` = kebab-case (e.g., `stock-management`)
### 2. Config File (REQUIRED)
**File**: `src/app/Modules/[ModuleName]/Config/[module_name].php`
```php
<?php
return [
'name' => '[Module Display Name]',
'slug' => '[module-slug]',
'version' => '1.0.0',
'audit' => [
'enabled' => true,
'strategy' => 'all', // 'all', 'include', 'exclude', 'none'
'include' => [],
'exclude' => [],
],
];
```
### 3. Model (REQUIRED for data modules)
**File**: `src/app/Modules/[ModuleName]/Models/[ModelName].php`
```php
<?php
namespace App\Modules\[ModuleName]\Models;
use App\Traits\ModuleAuditable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use OwenIt\Auditing\Contracts\Auditable;
class [ModelName] extends Model implements Auditable
{
use HasFactory, ModuleAuditable;
protected $fillable = [
// List ALL fields that can be mass-assigned
];
protected $casts = [
// Type casts for fields
];
// Define relationships here
}
```
### 4. Migration (REQUIRED for data modules)
**File**: `src/app/Modules/[ModuleName]/Database/Migrations/YYYY_MM_DD_HHMMSS_create_[table_name]_table.php`
```php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('[table_name]', function (Blueprint $table) {
$table->id();
// Define ALL columns here
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('[table_name]');
}
};
```
### 5. Permissions (REQUIRED) - AUTO-LOADED
**File**: `src/app/Modules/[ModuleName]/Permissions.php`
```php
<?php
return [
'[module_name].view' => 'View [Module Name]',
'[module_name].create' => 'Create [Module Name] records',
'[module_name].edit' => 'Edit [Module Name] records',
'[module_name].delete' => 'Delete [Module Name] records',
];
```
> **✅ AUTO-LOADED**: `RolePermissionSeeder` automatically scans all `app/Modules/*/Permissions.php`
> files and registers them. Just run `php artisan db:seed --class=RolePermissionSeeder`.
### 6. Filament Resource (REQUIRED for admin)
**File**: `src/app/Modules/[ModuleName]/Filament/Resources/[ModelName]Resource.php`
```php
<?php
namespace App\Modules\[ModuleName]\Filament\Resources;
use App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource\Pages;
use App\Modules\[ModuleName]\Models\[ModelName];
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class [ModelName]Resource extends Resource
{
protected static ?string $model = [ModelName]::class;
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationGroup = '[Module Display Name]';
protected static ?int $navigationSort = 1;
public static function canAccess(): bool
{
return auth()->user()?->can('[module_name].view') ?? false;
}
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Section::make('[Model Name] Details')
->schema([
// Add form fields here
]),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
// Add table columns here
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\List[ModelName]s::route('/'),
'create' => Pages\Create[ModelName]::route('/create'),
'edit' => Pages\Edit[ModelName]::route('/{record}/edit'),
];
}
}
```
### 7. Filament Resource Pages (REQUIRED)
**File**: `src/app/Modules/[ModuleName]/Filament/Resources/[ModelName]Resource/Pages/List[ModelName]s.php`
```php
<?php
namespace App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource\Pages;
use App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class List[ModelName]s extends ListRecords
{
protected static string $resource = [ModelName]Resource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
```
**File**: `src/app/Modules/[ModuleName]/Filament/Resources/[ModelName]Resource/Pages/Create[ModelName].php`
```php
<?php
namespace App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource\Pages;
use App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource;
use Filament\Resources\Pages\CreateRecord;
class Create[ModelName] extends CreateRecord
{
protected static string $resource = [ModelName]Resource::class;
}
```
**File**: `src/app/Modules/[ModuleName]/Filament/Resources/[ModelName]Resource/Pages/Edit[ModelName].php`
```php
<?php
namespace App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource\Pages;
use App\Modules\[ModuleName]\Filament\Resources\[ModelName]Resource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class Edit[ModelName] extends EditRecord
{
protected static string $resource = [ModelName]Resource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}
```
### 8. Routes (REQUIRED)
**File**: `src/app/Modules/[ModuleName]/Routes/web.php`
```php
<?php
use App\Modules\[ModuleName]\Http\Controllers\[ModuleName]Controller;
use Illuminate\Support\Facades\Route;
Route::get('/', [[ModuleName]Controller::class, 'index'])->name('index');
```
**File**: `src/app/Modules/[ModuleName]/Routes/api.php` (if API needed)
```php
<?php
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->group(function () {
// API routes here
});
```
### 9. Controller (REQUIRED)
**File**: `src/app/Modules/[ModuleName]/Http/Controllers/[ModuleName]Controller.php`
```php
<?php
namespace App\Modules\[ModuleName]\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class [ModuleName]Controller extends Controller
{
public function index()
{
$this->authorize('[module_name].view');
return view('[module-slug]::index');
}
}
```
### 10. Index View (REQUIRED)
**File**: `src/app/Modules/[ModuleName]/Resources/views/index.blade.php`
```blade
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('[Module Display Name]') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
{{ __('[Module Display Name] content goes here.') }}
</div>
</div>
</div>
</div>
</x-app-layout>
```
---
## ✅ MODULE REGISTRATION
After creating all files, register the module in `src/config/app.php` providers array:
```php
'providers' => ServiceProvider::defaultProviders()->merge([
// ...existing providers...
App\Modules\[ModuleName]\[ModuleName]ServiceProvider::class,
])->toArray(),
```
Or add to `src/bootstrap/providers.php`:
```php
return [
App\Providers\AppServiceProvider::class,
// ...existing providers...
App\Modules\[ModuleName]\[ModuleName]ServiceProvider::class,
];
```
---
## 🔄 AFTER MODULE CREATION - REQUIRED COMMANDS
```bash
# 1. Run migrations
docker-compose exec app php artisan migrate
# 2. Clear all caches
docker-compose exec app php artisan optimize:clear
# 3. Seed permissions (if using permissions)
docker-compose exec app php artisan db:seed --class=RolePermissionSeeder
# 4. Reset permission cache
docker-compose exec app php artisan permission:cache-reset
```
---
## ⚠️ COMMON MISTAKES TO AVOID
### 1. Wrong Namespace
`namespace App\Modules\StockManagement\Models;`
`namespace App\Modules\StockManagement\Models;`
The namespace MUST match the folder structure EXACTLY.
### 2. Wrong View Namespace
`return view('stockmanagement::index');`
`return view('StockManagement::index');`
`return view('stock-management::index');`
View namespace is ALWAYS kebab-case.
### 3. Wrong Permission Names
`'StockManagement.view'`
`'stock-management.view'`
`'stock_management.view'`
Permissions are ALWAYS snake_case.
### 4. Forgetting to Register Provider
The module WILL NOT LOAD if you forget to add its ServiceProvider.
### 5. Wrong Import Paths
`use App\Models\Product;`
`use App\Modules\StockManagement\Models\Product;`
Module models are in the MODULE namespace, not the core App namespace.
### 6. Missing Fillable Array
❌ Empty `$fillable` array causes mass-assignment errors
✅ List ALL fields that should be mass-assignable
### 7. Forgetting to Run Migrations
Always run `php artisan migrate` after creating migrations.
---
## 🎨 FILAMENT FORM FIELD REFERENCE
```php
// Text
Forms\Components\TextInput::make('name')
->required()
->maxLength(255);
// Email
Forms\Components\TextInput::make('email')
->email()
->required();
// Password
Forms\Components\TextInput::make('password')
->password()
->required();
// Textarea
Forms\Components\Textarea::make('description')
->rows(3);
// Select
Forms\Components\Select::make('status')
->options([
'active' => 'Active',
'inactive' => 'Inactive',
])
->required();
// Checkbox
Forms\Components\Toggle::make('is_active')
->default(true);
// Date
Forms\Components\DatePicker::make('date');
// DateTime
Forms\Components\DateTimePicker::make('published_at');
// Number
Forms\Components\TextInput::make('price')
->numeric()
->prefix('$');
// File Upload
Forms\Components\FileUpload::make('image')
->image()
->directory('uploads');
// Relationship Select
Forms\Components\Select::make('category_id')
->relationship('category', 'name')
->searchable()
->preload();
```
---
## 📊 FILAMENT TABLE COLUMN REFERENCE
```php
// Text
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable();
// Badge
Tables\Columns\TextColumn::make('status')
->badge()
->color(fn (string $state): string => match ($state) {
'active' => 'success',
'inactive' => 'danger',
default => 'gray',
});
// Boolean Icon
Tables\Columns\IconColumn::make('is_active')
->boolean();
// Image
Tables\Columns\ImageColumn::make('avatar')
->circular();
// Date
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable();
// Money
Tables\Columns\TextColumn::make('price')
->money('USD');
```
---
## 🔐 EXISTING CORE RESOURCES
### User Management (already exists in template)
- `src/app/Filament/Resources/UserResource.php` - User CRUD with role assignment
- `src/app/Filament/Resources/RoleResource.php` - Role CRUD with permissions
- `src/app/Filament/Resources/PermissionResource.php` - Permission CRUD
### Settings (already exists)
- `src/app/Filament/Pages/Settings.php` - Site settings (name, logo, colors, registration toggle)
- `src/app/Models/Setting.php` - Settings model with get/set helpers
### Authentication (pre-installed)
- Laravel Breeze with Blade templates
- Login: `/login`
- Register: `/register` (controlled by enable_registration setting)
- Admin: `/admin`
---
## 🧪 TESTING MODULES
Create tests in `src/tests/Feature/Modules/[ModuleName]/`:
```php
<?php
namespace Tests\Feature\Modules\[ModuleName];
use App\Models\User;
use App\Modules\[ModuleName]\Models\[ModelName];
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class [ModelName]Test extends TestCase
{
use RefreshDatabase;
public function test_user_can_view_[model_name]s(): void
{
$user = User::factory()->create();
$user->givePermissionTo('[module_name].view');
$this->actingAs($user)
->get('/[module-slug]')
->assertStatus(200);
}
}
```
---
## 📋 CHECKLIST FOR NEW MODULES
Before considering a module complete:
- [ ] ServiceProvider created and registered
- [ ] Config file created
- [ ] Model created with fillable and casts
- [ ] Migration created and run
- [ ] Permissions.php created
- [ ] Filament Resource created with all pages
- [ ] Controller created
- [ ] Routes (web.php and optionally api.php) created
- [ ] Index view created
- [ ] `php artisan migrate` run
- [ ] `php artisan optimize:clear` run
- [ ] `php artisan db:seed --class=RolePermissionSeeder` run
- [ ] Module accessible at `/[module-slug]`
- [ ] Admin panel shows module in navigation
- [ ] CRUD operations work in admin
---
## 🆘 DEBUGGING
### Module Not Loading
```bash
# Check if provider is registered
docker-compose exec app php artisan about
# Clear everything
docker-compose exec app php artisan optimize:clear
```
### Filament Resource Not Showing
```bash
# Clear Filament cache
docker-compose exec app php artisan filament:cache-components
# Check canAccess() method returns true
# Check user has required permission
```
### Permission Denied
```bash
# Reset permission cache
docker-compose exec app php artisan permission:cache-reset
# Verify permissions exist
docker-compose exec app php artisan tinker
>>> \Spatie\Permission\Models\Permission::pluck('name');
```
### View Not Found
```bash
# Verify view namespace (must be kebab-case)
# Check file exists in Resources/views/
```
---
**Remember**: When in doubt, look at existing code in the codebase. The patterns are already established - follow them exactly.

331
DEBUGGING.md Normal file
View File

@@ -0,0 +1,331 @@
# Debugging Strategy
**CRITICAL**: This template includes enhanced error tracking. Always check logs FIRST before guessing at solutions.
## 🚨 Golden Rule: Logs First, Guessing Never
When encountering errors:
### ❌ WRONG Approach:
1. See error message
2. Guess at the cause
3. Try random fixes
4. Waste time
### ✅ CORRECT Approach:
1. **Check Laravel logs immediately**
2. Read the full stack trace
3. Identify the exact file and line
4. Fix the root cause
---
## Error Tracking Suite
This template includes:
- **Spatie Laravel Ignition** v2.11.0 - Enhanced error pages
- **Spatie Flare Client** v1.10.1 - Error tracking
- **Laravel Logs** - Full context and stack traces
---
## How to Debug Errors
### 1. Check Laravel Logs (PRIMARY METHOD)
```bash
# View recent errors
docker-compose exec app tail -n 100 storage/logs/laravel.log
# Search for specific error
docker-compose exec app cat storage/logs/laravel.log | grep "ErrorType"
# Watch logs in real-time
docker-compose exec app tail -f storage/logs/laravel.log
```
**What you'll see:**
- Exact file path and line number
- Full stack trace
- Variable values at time of error
- User context (if logged in)
### 2. Check Container Logs
```bash
# Application logs
docker-compose logs --tail=50 app
# Nginx logs (for 502/504 errors)
docker-compose logs --tail=50 nginx
# All services
docker-compose logs --tail=50
```
### 3. Use Ignition Error Pages (Development)
When `APP_DEBUG=true`, Laravel shows beautiful error pages with:
- Code context (lines around the error)
- Stack trace with clickable frames
- Solution suggestions for common errors
- Request data and environment info
**Access**: Errors automatically show in browser during development
---
## Common Error Patterns
### TypeError: htmlspecialchars() expects string, array given
**Log Location**: `storage/logs/laravel.log`
**What to look for**:
```
htmlspecialchars(): Argument #1 ($string) must be of type string, array given
at /var/www/html/path/to/file.blade.php:LINE
```
**Root Cause**: Blade template trying to echo an array with `{{ }}`
**Fix**: Use proper component or `@json()` directive
---
### 504 Gateway Timeout
**Log Location**:
- `docker-compose logs nginx`
- `storage/logs/laravel.log`
**What to look for**:
```
upstream timed out (110: Operation timed out)
```
**Common Causes**:
- Stale cache after package installation
- Infinite loop in code
- Database query timeout
- Permission cache issues
**Fix**:
```bash
docker-compose exec app php artisan cache:clear
docker-compose exec app php artisan config:clear
docker-compose exec app php artisan view:clear
docker-compose exec app php artisan permission:cache-reset
docker-compose restart app nginx
```
---
### Database Connection Errors
**Log Location**: `storage/logs/laravel.log`
**What to look for**:
```
SQLSTATE[HY000] [2002] Connection refused
```
**Fix**:
```bash
# Check if database is running
docker-compose ps
# Check database logs
docker-compose logs mysql
# Restart database
docker-compose restart mysql
# Wait for database to be ready
docker-compose exec mysql mysqladmin ping -h localhost
```
---
### Redis Connection Errors
**Log Location**: `storage/logs/laravel.log`
**What to look for**:
```
php_network_getaddresses: getaddrinfo for redis failed
```
**Fix**:
```bash
# Check Redis is running
docker-compose ps redis
# Restart Redis
docker-compose restart redis
# Clear config cache
docker-compose exec app php artisan config:clear
```
---
## Debugging Workflow for AI Agents
When a user reports an error, follow this exact sequence:
### Step 1: Get the Full Error
```bash
docker-compose exec app cat storage/logs/laravel.log | grep -A 20 "ErrorType"
```
### Step 2: Identify Root Cause
- Read the stack trace
- Find the originating file and line
- Understand what the code is trying to do
### Step 3: Verify the Fix
- Make the change
- Clear relevant caches
- Test the specific scenario that caused the error
### Step 4: Document
- Explain what was wrong
- Show the fix
- Explain why it works
---
## Cache Clearing Commands
Always clear caches after:
- Installing new packages
- Changing configuration
- Modifying service providers
- Updating permissions/roles
```bash
# Clear all caches
docker-compose exec app php artisan optimize:clear
# Or individually
docker-compose exec app php artisan cache:clear
docker-compose exec app php artisan config:clear
docker-compose exec app php artisan route:clear
docker-compose exec app php artisan view:clear
docker-compose exec app php artisan permission:cache-reset
# Regenerate autoloader
docker-compose exec app composer dump-autoload
```
---
## Performance Debugging
### Slow Page Loads
**Check**:
1. Laravel Debugbar (if installed)
2. Query count and time
3. OPcache status
4. Redis connection
**Commands**:
```bash
# Check OPcache status
docker-compose exec app php -i | grep opcache
# Monitor Redis
docker-compose exec redis redis-cli monitor
# Check query logs
# Enable in config/database.php: 'log_queries' => true
```
---
## Production Error Tracking
For production, consider:
1. **Flare** (https://flareapp.io)
- Add `FLARE_KEY` to `.env`
- Automatic error reporting
- Error grouping and notifications
2. **Sentry** (alternative)
```bash
composer require sentry/sentry-laravel
```
3. **Log Aggregation**
- Papertrail
- Loggly
- CloudWatch
---
## Best Practices
### ✅ DO:
- Check logs before making changes
- Read the full stack trace
- Clear caches after changes
- Test the specific error scenario
- Document the fix
### ❌ DON'T:
- Guess at solutions without checking logs
- Make multiple changes at once
- Skip cache clearing
- Assume the error message tells the whole story
- Leave debugging code in production
---
## Quick Reference
| Error Type | First Check | Quick Fix |
|------------|-------------|-----------|
| TypeError | Laravel logs | Check variable types |
| 504 Timeout | Nginx logs | Clear caches, restart |
| Database | MySQL logs | Check connection, restart DB |
| Redis | Laravel logs | Restart Redis, clear config |
| Permission | Laravel logs | `permission:cache-reset` |
| View | Laravel logs | `view:clear` |
| Route | Laravel logs | `route:clear` |
---
## Log File Locations
| Service | Log Location |
|---------|--------------|
| Laravel | `storage/logs/laravel.log` |
| Nginx | Docker logs: `docker-compose logs nginx` |
| PHP-FPM | Docker logs: `docker-compose logs app` |
| MySQL | Docker logs: `docker-compose logs mysql` |
| Redis | Docker logs: `docker-compose logs redis` |
---
## Emergency Commands
When everything is broken:
```bash
# Nuclear option - reset everything
docker-compose down
docker-compose up -d
docker-compose exec app php artisan optimize:clear
docker-compose exec app composer dump-autoload
docker-compose exec app php artisan migrate:fresh --seed
```
**⚠️ WARNING**: `migrate:fresh` will delete all data!
---
**Remember**: Logs are your friend. Always check them first. 🔍

272
FEATURES.md Normal file
View File

@@ -0,0 +1,272 @@
# Installed Features
This document lists all features installed in this Laravel Docker Development Template.
## ✅ Complete Feature List
### 1. **Permissions & Roles** (spatie/laravel-permission)
- **Version**: 6.24.1
- **Features**:
- Role-based access control
- Pre-configured roles: admin, editor, viewer
- Permission system for granular access
- User model integration with `HasRoles` trait
- **Usage**:
```php
// Assign role
$user->assignRole('admin');
// Check permission
if ($user->can('users.edit')) { }
// Check role
if ($user->hasRole('admin')) { }
```
- **Database Tables**: `roles`, `permissions`, `model_has_roles`, `model_has_permissions`, `role_has_permissions`
---
### 2. **Audit Trail** (owen-it/laravel-auditing)
- **Version**: 14.0.0
- **Features**:
- Track all model changes (create, update, delete)
- Record user who made changes
- Store old and new values
- Audit log with timestamps
- **Usage**:
```php
use OwenIt\Auditing\Contracts\Auditable;
class Product extends Model implements Auditable
{
use \OwenIt\Auditing\Auditable;
}
// View audits
$audits = $product->audits;
```
- **Database Table**: `audits`
---
### 3. **Error Tracking** (spatie/laravel-ignition + spatie/flare-client-php)
- **Versions**:
- spatie/laravel-ignition: 2.11.0
- spatie/flare-client-php: 1.10.1
- spatie/ignition: 1.15.1
- **Features**:
- Beautiful error pages in development
- Stack trace with code context
- Solution suggestions for common errors
- Optional Flare integration for production error tracking
- **Configuration**: Already active in development mode
---
### 4. **API Authentication** (laravel/sanctum)
- **Version**: 4.3.1
- **Features**:
- Token-based API authentication
- SPA authentication
- Mobile app authentication
- API token management
- **Usage**:
```php
// Generate token
$token = $user->createToken('api-token')->plainTextToken;
// In routes/api.php
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
```
- **User Model**: Updated with `HasApiTokens` trait
- **Database Table**: `personal_access_tokens`
---
### 5. **Site Settings**
- **Features**:
- Logo upload
- Color scheme (primary, secondary, accent)
- Site name and description
- Contact email
- Maintenance mode toggle
- **Location**: `/admin/settings`
- **Usage**:
```php
// Get setting
$siteName = Setting::get('site_name', 'Default');
// Set setting
Setting::set('primary_color', '#3b82f6');
```
- **Files**:
- Model: `app/Models/Setting.php`
- Page: `app/Filament/Pages/Settings.php`
- Migration: `database/migrations/2026_03_09_022522_create_settings_table.php`
---
### 6. **Module System**
- **Features**:
- Artisan command to scaffold complete modules
- Auto-generates: Model, Controller, Routes, Views, Migration, Tests, Filament Resource
- Modular architecture for organizing features
- Blade templates with Tailwind CSS
- **Usage**:
```bash
php artisan make:module ProductCatalog
```
- **Documentation**: `app/Modules/README.md`
- **Command**: `app/Console/Commands/MakeModuleCommand.php`
---
### 7. **Filament Admin Panel**
- **Version**: 3.3
- **Features**:
- User management resource
- Site settings page
- Dashboard with widgets
- Form and table builders
- Dark mode support
- **Access**: http://localhost:8080/admin
- **Credentials**: admin@example.com / password
---
### 8. **Laravel Breeze**
- **Version**: 2.3
- **Features**:
- Login, register, password reset
- Email verification
- Profile management
- Blade templates with Tailwind CSS
- Dark mode support
---
### 9. **Pest Testing Framework**
- **Version**: 3.8
- **Features**:
- Modern testing syntax
- Laravel integration
- Example tests included
- Test helpers for permissions and modules
- **Usage**:
```bash
php artisan test
# or
./vendor/bin/pest
```
---
### 10. **Performance Optimizations**
- **OPcache**: Enabled with development-friendly settings
- **Redis**: Configured for cache and queues
- **Volume Mounts**: Optimized with `:cached` flag for WSL2
- **Config**:
- `CACHE_STORE=redis`
- `SESSION_DRIVER=database`
- `QUEUE_CONNECTION=redis`
---
## Pre-Configured Roles & Permissions
### Roles
1. **Admin** - Full access to all features
2. **Editor** - Can view and edit users
3. **Viewer** - Read-only access to users
### Permissions
- `users.view`
- `users.create`
- `users.edit`
- `users.delete`
- `settings.manage`
---
## Database Tables Created
1. `users` - User accounts
2. `sessions` - User sessions
3. `cache` - Cache storage
4. `jobs` - Queue jobs
5. `failed_jobs` - Failed queue jobs
6. `password_reset_tokens` - Password resets
7. `settings` - Site configuration
8. `roles` - User roles
9. `permissions` - Access permissions
10. `model_has_roles` - User-role assignments
11. `model_has_permissions` - User-permission assignments
12. `role_has_permissions` - Role-permission assignments
13. `audits` - Audit trail logs
14. `personal_access_tokens` - API tokens
---
## Access Points
| Feature | URL | Credentials |
|---------|-----|-------------|
| Public Site | http://localhost:8080 | - |
| Admin Panel | http://localhost:8080/admin | admin@example.com / password |
| Site Settings | http://localhost:8080/admin/settings | Admin access required |
| Email Testing | http://localhost:8025 | - |
| API Endpoints | http://localhost:8080/api/* | Requires Sanctum token |
---
## Next Steps
1. **Customize Site Settings** - Set your logo and brand colors
2. **Create Modules** - Use `php artisan make:module` to build features
3. **Assign Roles** - Give users appropriate access levels
4. **Build API** - Create API endpoints with Sanctum authentication
5. **Write Tests** - Add tests for your custom features
6. **Enable Auditing** - Add `Auditable` interface to models you want to track
7. **Deploy** - See production deployment guide in README.md
---
## Documentation
- [GETTING_STARTED.md](GETTING_STARTED.md) - Setup and configuration
- [README.md](README.md) - Overview and commands
- [app/Modules/README.md](src/app/Modules/README.md) - Module system guide
- [AI_CONTEXT.md](AI_CONTEXT.md) - AI assistant context
---
## Package Versions
All packages are installed and configured:
```json
{
"require": {
"php": "^8.2",
"filament/filament": "^3.2",
"laravel/framework": "^11.31",
"laravel/sanctum": "^4.3",
"laravel/tinker": "^2.9",
"owen-it/laravel-auditing": "^14.0",
"spatie/flare-client-php": "^1.10",
"spatie/laravel-ignition": "^2.11",
"spatie/laravel-permission": "^6.24"
},
"require-dev": {
"laravel/breeze": "^2.3",
"pestphp/pest": "^3.8",
"pestphp/pest-plugin-laravel": "^3.2"
}
}
```
---
**Last Updated**: March 9, 2026

364
GETTING_STARTED.md Normal file
View File

@@ -0,0 +1,364 @@
# Getting Started
This guide walks you through setting up your Laravel development environment using this template.
## Prerequisites
- Docker & Docker Compose
- Git
## Quick Start (2 minutes)
**Everything is pre-installed!** Just clone and run:
```bash
# 1. Clone the template
git clone https://github.com/your-repo/Laravel-Docker-Dev-Template.git my-project
cd my-project
# 2. Run setup (MySQL is default)
./setup.sh # Linux/Mac
setup.bat # Windows
# Or choose a different database:
./setup.sh pgsql # PostgreSQL
./setup.sh sqlite # SQLite
# 3. Access your app
# Laravel: http://localhost:8080
# Admin: http://localhost:8080/admin (admin@example.com / password)
# Mail: http://localhost:8025
```
**That's it!** You now have a fully working Laravel application with:
- ✅ Authentication (login, register, password reset)
- ✅ Admin panel with user management
- ✅ Testing framework (Pest)
- ✅ Code style (Pint)
- ✅ Email testing (Mailpit)
## What's Pre-Installed
This template comes with everything configured and ready to use:
### Core Framework
- **Laravel 11** - Latest version with all features
- **Laravel Breeze** - Authentication scaffolding (Blade + dark mode)
- **Livewire** - Reactive components without JavaScript
### Admin & Management
- **Filament v3.3** - Full-featured admin panel
- **User Management** - CRUD interface for users
- **Site Settings** - Logo, color scheme, and site configuration
- **Spatie Permissions** - Role-based access control (admin, editor, viewer)
- **Dashboard** - Admin dashboard with widgets
### Security & Tracking
- **Laravel Auditing** - Track all data changes and user actions
- **Laravel Sanctum** - API authentication with tokens
- **Spatie Ignition** - Enhanced error pages and debugging
### Development Tools
- **Pest** - Modern testing framework with elegant syntax
- **Laravel Pint** - Opinionated code style fixer
- **Mailpit** - Email testing tool
- **Module System** - Artisan command to scaffold new features
### Infrastructure
- **Docker** - Containerized development environment
- **MySQL/PostgreSQL/SQLite** - Choose your database
- **Redis** - Caching and queues (OPcache enabled)
- **Queue Workers** - Background job processing (optional)
- **Scheduler** - Task scheduling (optional)
## Step-by-Step Setup
### 1. Clone the Repository
```bash
git clone https://github.com/your-repo/Laravel-Docker-Dev-Template.git my-project
cd my-project
# Optional: Remove template git history and start fresh
rm -rf .git
git init
git add .
git commit -m "Initial commit"
```
### 2. Choose Your Database
| Database | Best For | Command |
|----------|----------|---------|
| **MySQL** | Most projects, production parity | `./setup.sh mysql` |
| **PostgreSQL** | Advanced features, JSON, full-text search | `./setup.sh pgsql` |
| **SQLite** | Simple apps, quick prototyping | `./setup.sh sqlite` |
### 3. Run Setup Script
```bash
./setup.sh mysql # Linux/Mac
setup.bat mysql # Windows
```
The script will:
- ✅ Configure environment for chosen database
- ✅ Install composer dependencies
- ✅ Build and start Docker containers
- ✅ Run database migrations
- ✅ Create admin user automatically
### 4. Start Developing
Your application is now ready! The setup script created an admin user for you:
**Admin Login:**
- Email: `admin@example.com`
- Password: `password`
**Access Points:**
- Public site: http://localhost:8080
- Admin panel: http://localhost:8080/admin
- Site settings: http://localhost:8080/admin/settings
- Email testing: http://localhost:8025
- API endpoints: http://localhost:8080/api/*
## Common Commands
```bash
# Shell into container
make shell
# Create a module with a model
php artisan make:module Inventory --model=Product
# Run migrations
php artisan migrate
# Seed permissions
php artisan db:seed --class=PermissionSeeder
```
Your module is now at:
- Frontend: http://localhost:8080/inventory
- Admin: http://localhost:8080/admin → Inventory section
## Project Structure After Setup
```
my-project/
├── src/ # Laravel application
│ ├── app/
│ │ ├── Modules/ # Your feature modules
│ │ │ └── Inventory/
│ │ │ ├── Config/
│ │ │ ├── Database/
│ │ │ ├── Filament/ # Admin resources
│ │ │ ├── Http/
│ │ │ ├── Models/
│ │ │ └── Routes/
│ │ ├── Providers/
│ │ └── Traits/
│ │ └── ModuleAuditable.php
│ ├── config/
│ ├── database/
│ ├── resources/
│ │ └── views/
│ │ └── errors/ # Custom error pages
│ └── routes/
├── docker/ # Docker configuration
├── deploy/ # Production deployment
└── docs/ # Documentation
```
## Common Commands
### Development
| Command | Description |
|---------|-------------|
| `make up` | Start containers |
| `make down` | Stop containers |
| `make shell` | Shell into app container |
| `make logs` | View container logs |
| `make artisan cmd='...'` | Run artisan command |
| `make composer cmd='...'` | Run composer command |
### Database
| Command | Description |
|---------|-------------|
| `make migrate` | Run migrations |
| `make fresh` | Fresh migrate + seed |
| `make tinker` | Laravel Tinker REPL |
### Testing & Quality
| Command | Description |
|---------|-------------|
| `make test` | Run tests |
| `make lint` | Fix code style (Pint) |
| `make lint-check` | Check code style |
### Queues & Backup
| Command | Description |
|---------|-------------|
| `make queue-start` | Start background job worker |
| `make queue-logs` | View queue worker logs |
| `make scheduler-start` | Start Laravel scheduler |
| `make backup` | Backup database |
| `make restore file=...` | Restore from backup |
### Modules
```bash
# Create a complete module with CRUD, views, tests, and Filament resource
php artisan make:module ProductCatalog
# This creates:
# - Model, Controller, Routes, Views
# - Migration and Filament Resource
# - Pest tests
# - See app/Modules/README.md for details
```
## Environment Files
| File | Purpose |
|------|---------|
| `.env` | Docker Compose config |
| `src/.env` | Laravel app config |
| `src/.env.mysql` | MySQL preset |
| `src/.env.pgsql` | PostgreSQL preset |
| `src/.env.sqlite` | SQLite preset |
### Key Environment Variables
```env
# App
APP_NAME="My App"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8080
# Database (set by profile)
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret
# Error Tracking
FLARE_KEY=your_key_here
# Ignition (dev)
IGNITION_THEME=auto
IGNITION_EDITOR=vscode
```
## Ports
| Service | Port | URL |
|---------|------|-----|
| Laravel | 8080 | http://localhost:8080 |
| MySQL | 3306 | localhost:3306 |
| PostgreSQL | 5432 | localhost:5432 |
| Redis | 6379 | localhost:6379 |
| Mailpit UI | 8025 | http://localhost:8025 |
| Mailpit SMTP | 1025 | localhost:1025 |
## Troubleshooting
### 🚨 FIRST STEP: Check Logs
**Always check logs before trying fixes:**
```bash
# Laravel application logs (MOST IMPORTANT)
docker-compose exec app tail -n 100 storage/logs/laravel.log
# Search for specific error
docker-compose exec app cat storage/logs/laravel.log | grep "ErrorType"
# Container logs
docker-compose logs --tail=50 app
docker-compose logs --tail=50 nginx
```
**See [DEBUGGING.md](DEBUGGING.md) for complete debugging guide.**
---
### Application Errors
When you see errors in the browser:
1. **Check Laravel logs** (see above)
2. **Read the full stack trace** - it shows exact file and line
3. **Clear caches** after making changes:
```bash
docker-compose exec app php artisan optimize:clear
docker-compose exec app php artisan permission:cache-reset
```
---
### Container won't start
```bash
# Check logs
make logs
# Reset everything
make clean
make install DB=mysql
```
### Permission issues
```bash
make shell
chmod -R 775 storage bootstrap/cache
```
### Database connection refused
```bash
# Wait for database to be ready
docker-compose exec mysql mysqladmin ping -h localhost
# Or restart
make down
make up
```
### Artisan commands fail
```bash
# Make sure you're in the container
make shell
# Then run artisan
php artisan migrate
```
## Next Steps
1. **Configure Site Settings** - Visit `/admin/settings` to set logo and colors
2. **Set Up Roles** - Assign users to admin/editor/viewer roles in Filament
3. **Create Modules** - `php artisan make:module YourFeature`
4. **Build API** - Use Sanctum tokens for API authentication
5. **Write Tests** - `php artisan test` or `./vendor/bin/pest`
6. **Configure Flare** - Optional: Get API key at [flareapp.io](https://flareapp.io)
7. **Deploy** - See [Production Deployment](README.md#production-deployment)
## Documentation
- [README.md](README.md) - Overview and commands
- [DEBUGGING.md](DEBUGGING.md) - **Debugging strategy (READ THIS FIRST)**
- [FEATURES.md](FEATURES.md) - Complete feature reference
- [AI_CONTEXT.md](AI_CONTEXT.md) - Context for AI assistants
- [app/Modules/README.md](src/app/Modules/README.md) - Module system guide

176
Makefile Normal file
View File

@@ -0,0 +1,176 @@
# Laravel Docker Development Makefile
.PHONY: help install up down restart build logs shell composer artisan npm test fresh
# Database profile (default: mysql)
DB ?= mysql
# Default target
help:
@echo "Laravel Docker Development Commands"
@echo "===================================="
@echo ""
@echo "Setup:"
@echo " make install DB=mysql - First-time setup with MySQL"
@echo " make install DB=pgsql - First-time setup with PostgreSQL"
@echo " make install DB=sqlite - First-time setup with SQLite"
@echo " make build - Build Docker images"
@echo ""
@echo "Docker (use DB=mysql|pgsql|sqlite):"
@echo " make up - Start containers (default: MySQL)"
@echo " make up DB=pgsql - Start with PostgreSQL"
@echo " make up DB=sqlite - Start with SQLite"
@echo " make down - Stop all containers"
@echo " make restart - Restart all containers"
@echo " make logs - View container logs"
@echo ""
@echo "Development:"
@echo " make shell - Open shell in app container"
@echo " make composer - Run composer (use: make composer cmd='install')"
@echo " make artisan - Run artisan (use: make artisan cmd='migrate')"
@echo " make npm - Run npm (use: make npm cmd='install')"
@echo " make tinker - Open Laravel Tinker"
@echo ""
@echo "Database:"
@echo " make migrate - Run migrations"
@echo " make fresh - Fresh migrate with seeders"
@echo " make seed - Run database seeders"
@echo ""
@echo "Testing:"
@echo " make test - Run PHPUnit tests"
@echo " make test-coverage - Run tests with coverage"
@echo ""
@echo "Frontend:"
@echo " make vite - Start Vite dev server"
@echo " make build-assets - Build frontend assets"
# Setup
install: build
@echo "Creating Laravel project..."
docker-compose --profile $(DB) run --rm app composer create-project laravel/laravel .
@echo "Setting up environment for $(DB)..."
cp src/.env.$(DB) src/.env || cp src/.env.example src/.env
docker-compose --profile $(DB) run --rm app php artisan key:generate
@echo ""
@echo "Installation complete!"
@echo "Run 'make up DB=$(DB)' to start with $(DB) database."
build:
docker-compose build
# Docker commands
up:
docker-compose --profile $(DB) up -d
@echo "Started with $(DB) database profile."
down:
docker-compose --profile mysql --profile pgsql --profile sqlite down
restart:
docker-compose --profile $(DB) restart
logs:
docker-compose logs -f
# Development
shell:
docker-compose exec app bash
composer:
docker-compose exec app composer $(cmd)
artisan:
docker-compose exec app php artisan $(cmd)
npm:
docker-compose run --rm node npm $(cmd)
tinker:
docker-compose exec app php artisan tinker
# Database
migrate:
docker-compose exec app php artisan migrate
fresh:
docker-compose exec app php artisan migrate:fresh --seed
seed:
docker-compose exec app php artisan db:seed
# Testing (Pest)
test:
docker-compose exec app php artisan test
test-coverage:
docker-compose exec app php artisan test --coverage
test-filter:
docker-compose exec app php artisan test --filter=$(filter)
test-module:
docker-compose exec app php artisan test tests/Modules/$(module)
test-parallel:
docker-compose exec app php artisan test --parallel
# Code Quality
lint:
docker-compose exec app ./vendor/bin/pint
lint-check:
docker-compose exec app ./vendor/bin/pint --test
# Frontend
vite:
docker-compose run --rm --service-ports node npm run dev
build-assets:
docker-compose run --rm node npm run build
# Post-install (run after Laravel is installed)
setup-tools:
docker-compose exec app bash -c "cd /var/www/html && bash /var/www/html/../scripts/post-install.sh"
# Laravel base setup (auth, API, middleware)
setup-laravel:
docker-compose exec app bash -c "cd /var/www/html && bash /var/www/html/../scripts/laravel-setup.sh"
# Full setup (tools + laravel)
setup-all: setup-tools setup-laravel
# Queue Worker
queue-start:
docker-compose --profile queue up -d queue
queue-stop:
docker-compose stop queue
queue-restart:
docker-compose restart queue
queue-logs:
docker-compose logs -f queue
# Scheduler
scheduler-start:
docker-compose --profile scheduler up -d scheduler
scheduler-stop:
docker-compose stop scheduler
# Database Backup/Restore
backup:
@bash scripts/backup.sh
restore:
@bash scripts/restore.sh $(file)
# Health Check
health:
docker-compose exec app php artisan health:check
# Cleanup
clean:
docker-compose down -v --remove-orphans
docker system prune -f

479
PRODUCTION_DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,479 @@
# Production Deployment Guide
**Target**: Ubuntu 24.04 with Apache/Nginx + PHP-FPM (NO Docker)
---
## Prerequisites on Production Server
### 1. Install Required Software
```bash
# Update system
sudo apt update && sudo apt upgrade -y
# Install PHP 8.3+ and extensions
sudo apt install -y php8.3 php8.3-fpm php8.3-cli php8.3-common \
php8.3-mysql php8.3-pgsql php8.3-sqlite3 \
php8.3-redis php8.3-curl php8.3-mbstring php8.3-xml \
php8.3-zip php8.3-bcmath php8.3-gd php8.3-intl
# Install web server (choose one)
sudo apt install -y apache2 # OR nginx
# Install database (choose one)
sudo apt install -y mysql-server # OR postgresql
# Install Redis
sudo apt install -y redis-server
# Install Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
# Install Node.js (for frontend assets)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
```
### 2. Verify PHP Extensions
```bash
php -m | grep -E "redis|pdo_mysql|mbstring|xml|curl|zip|bcmath|gd"
```
All should be listed. If not, install missing extensions.
---
## Deployment Steps
### 1. Clone Repository
```bash
cd /var/www
sudo git clone https://your-repo-url.git your-domain.com
cd your-domain.com/src
```
### 2. Set Permissions
```bash
sudo chown -R www-data:www-data /var/www/your-domain.com
sudo chmod -R 775 /var/www/your-domain.com/src/storage
sudo chmod -R 775 /var/www/your-domain.com/src/bootstrap/cache
```
### 3. Install Dependencies
```bash
# Composer dependencies
composer install --no-dev --optimize-autoloader
# Node dependencies and build assets
npm install
npm run build
```
### 4. Configure Environment
```bash
# Copy appropriate .env template
cp .env.mysql .env # or .env.pgsql or .env.sqlite
# Edit .env
nano .env
```
**Required .env settings:**
```env
APP_NAME="Your App Name"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=your_user
DB_PASSWORD=your_password
CACHE_STORE=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=database
SESSION_DOMAIN=.your-domain.com
SESSION_SECURE_COOKIE=true
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=your-smtp-host
MAIL_PORT=587
MAIL_USERNAME=your-email
MAIL_PASSWORD=your-password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@your-domain.com
MAIL_FROM_NAME="${APP_NAME}"
```
### 5. Generate Application Key
```bash
php artisan key:generate --force
```
### 6. Run Migrations and Seeders
```bash
# Run migrations
php artisan migrate --force
# CRITICAL: Run seeders to create roles, permissions, and admin user
php artisan db:seed --force
```
This creates:
- **Admin user**: admin@example.com / password
- **Roles**: admin, editor, viewer
- **Permissions**: users.view, users.create, users.edit, users.delete, settings.manage
### 7. Optimize for Production
```bash
# Cache configuration
php artisan config:cache
# Cache routes
php artisan route:cache
# Cache views
php artisan view:cache
# Optimize autoloader
composer dump-autoload --optimize
```
### 8. Create Storage Link
```bash
php artisan storage:link
```
---
## Web Server Configuration
### Option A: Apache with Virtual Host
Create `/etc/apache2/sites-available/your-domain.com.conf`:
```apache
<VirtualHost *:80>
ServerName your-domain.com
ServerAlias www.your-domain.com
DocumentRoot /var/www/your-domain.com/src/public
<Directory /var/www/your-domain.com/src/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/your-domain.com-error.log
CustomLog ${APACHE_LOG_DIR}/your-domain.com-access.log combined
# PHP-FPM
<FilesMatch \.php$>
SetHandler "proxy:unix:/var/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>
</VirtualHost>
```
Enable site and modules:
```bash
sudo a2enmod rewrite proxy_fcgi setenvif
sudo a2enconf php8.3-fpm
sudo a2ensite your-domain.com.conf
sudo systemctl restart apache2
```
### Option B: Nginx with Server Block
Create `/etc/nginx/sites-available/your-domain.com`:
```nginx
server {
listen 80;
listen [::]:80;
server_name your-domain.com www.your-domain.com;
root /var/www/your-domain.com/src/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
```
Enable site:
```bash
sudo ln -s /etc/nginx/sites-available/your-domain.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
```
---
## SSL Certificate (Let's Encrypt)
```bash
# Install Certbot
sudo apt install -y certbot python3-certbot-apache # For Apache
# OR
sudo apt install -y certbot python3-certbot-nginx # For Nginx
# Get certificate
sudo certbot --apache -d your-domain.com -d www.your-domain.com # Apache
# OR
sudo certbot --nginx -d your-domain.com -d www.your-domain.com # Nginx
# Auto-renewal is set up automatically
```
---
## Queue Worker Setup (Optional but Recommended)
Create `/etc/systemd/system/laravel-queue.service`:
```ini
[Unit]
Description=Laravel Queue Worker
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
Restart=always
ExecStart=/usr/bin/php /var/www/your-domain.com/src/artisan queue:work --sleep=3 --tries=3 --max-time=3600
[Install]
WantedBy=multi-user.target
```
Enable and start:
```bash
sudo systemctl enable laravel-queue
sudo systemctl start laravel-queue
sudo systemctl status laravel-queue
```
---
## Scheduler Setup
Add to crontab:
```bash
sudo crontab -e -u www-data
```
Add this line:
```
* * * * * cd /var/www/your-domain.com/src && php artisan schedule:run >> /dev/null 2>&1
```
---
## Post-Deployment Checklist
- [ ] PHP Redis extension installed: `php -m | grep redis`
- [ ] Database migrations run: `php artisan migrate --force`
- [ ] **Database seeded**: `php artisan db:seed --force` ✅ CRITICAL
- [ ] Storage permissions set: `chmod -R 775 storage bootstrap/cache`
- [ ] Storage link created: `php artisan storage:link`
- [ ] Config cached: `php artisan config:cache`
- [ ] Routes cached: `php artisan route:cache`
- [ ] Views cached: `php artisan view:cache`
- [ ] SSL certificate installed
- [ ] Queue worker running (if using queues)
- [ ] Scheduler configured (if using scheduled tasks)
- [ ] Admin user created and can login at `/admin`
- [ ] `.env` has `APP_DEBUG=false` and `APP_ENV=production`
---
## Access Your Application
- **Public Site**: https://your-domain.com
- **Admin Panel**: https://your-domain.com/admin
- **Admin Login**: admin@example.com / password
**⚠️ IMPORTANT**: Change the default admin password immediately after first login!
---
## Troubleshooting
### 500 Error - Check Logs
```bash
# Laravel logs
tail -f /var/www/your-domain.com/src/storage/logs/laravel.log
# Apache logs
sudo tail -f /var/log/apache2/your-domain.com-error.log
# Nginx logs
sudo tail -f /var/log/nginx/error.log
# PHP-FPM logs
sudo tail -f /var/log/php8.3-fpm.log
```
### Class "Redis" not found
```bash
# Install PHP Redis extension
sudo apt install php8.3-redis
# Restart PHP-FPM
sudo systemctl restart php8.3-fpm
# Restart web server
sudo systemctl restart apache2 # or nginx
```
### 419 Page Expired (CSRF)
Check `.env`:
```env
SESSION_DOMAIN=.your-domain.com
SESSION_SECURE_COOKIE=true
APP_URL=https://your-domain.com
```
Clear cache:
```bash
php artisan config:clear
php artisan cache:clear
```
### Roles Don't Exist
```bash
# Run the seeder
php artisan db:seed --class=RolePermissionSeeder
# Or run all seeders
php artisan db:seed --force
```
### Permission Denied Errors
```bash
sudo chown -R www-data:www-data /var/www/your-domain.com
sudo chmod -R 775 /var/www/your-domain.com/src/storage
sudo chmod -R 775 /var/www/your-domain.com/src/bootstrap/cache
```
---
## Updating the Application
```bash
cd /var/www/your-domain.com
# Pull latest code
sudo -u www-data git pull
# Update dependencies
cd src
composer install --no-dev --optimize-autoloader
npm install && npm run build
# Run migrations
php artisan migrate --force
# Clear and recache
php artisan config:clear
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Restart queue worker
sudo systemctl restart laravel-queue
# Restart web server
sudo systemctl restart apache2 # or nginx
```
---
## Security Recommendations
1. **Change default admin password** immediately
2. **Set up firewall**: `sudo ufw enable && sudo ufw allow 80,443/tcp`
3. **Disable directory listing** in web server config
4. **Keep system updated**: `sudo apt update && sudo apt upgrade`
5. **Use strong database passwords**
6. **Enable fail2ban**: `sudo apt install fail2ban`
7. **Regular backups** of database and uploaded files
8. **Monitor logs** for suspicious activity
---
## Backup Strategy
### Database Backup
```bash
# MySQL
mysqldump -u username -p database_name > backup_$(date +%Y%m%d).sql
# PostgreSQL
pg_dump -U username database_name > backup_$(date +%Y%m%d).sql
```
### Files Backup
```bash
# Backup storage directory
tar -czf storage_backup_$(date +%Y%m%d).tar.gz /var/www/your-domain.com/src/storage
```
### Automated Backups
Add to crontab:
```bash
0 2 * * * /path/to/backup-script.sh
```
---
**Remember**: The template is designed for quick local development with Docker. Production deployment requires proper server setup and security hardening.

589
README.md Normal file
View File

@@ -0,0 +1,589 @@
# Laravel Docker Development Template
A comprehensive Laravel development environment with Docker for local development and deployment configurations for Ubuntu 24.04 with Nginx Proxy Manager or Apache.
> **New here?** Start with [GETTING_STARTED.md](GETTING_STARTED.md) for a step-by-step setup guide.
>
> **AI Assistant?** See [CLAUDE.md](CLAUDE.md) for EXACT module templates and step-by-step instructions.
> Also see [AI_CONTEXT.md](AI_CONTEXT.md) for project context and conventions.
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ DEVELOPMENT (Docker) │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────┐ ┌─────────┐ ┌───────────────┐ ┌─────┐ ┌─────┐ │
│ │ Nginx │──│ PHP │──│ MySQL/PgSQL/ │ │Redis│ │Mail │ │
│ │ :8080 │ │ FPM │ │ SQLite │ │:6379│ │:8025│ │
│ └─────────┘ └─────────┘ └───────────────┘ └─────┘ └─────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ PRODUCTION (Ubuntu 24.04 - No Docker) │
├─────────────────────────────────────────────────────────────────┤
│ Option A: Nginx Proxy Manager │
│ ┌───────────────┐ ┌─────────┐ ┌─────────┐ │
│ │ NPM (SSL/443) │───▶│ Nginx │───▶│ PHP-FPM │───▶ Laravel │
│ └───────────────┘ └─────────┘ └─────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Option B: Apache Virtual Host │
│ ┌─────────────────┐ ┌─────────┐ │
│ │ Apache + SSL │───▶│ PHP-FPM │───▶ Laravel │
│ │ (Certbot) │ └─────────┘ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## Project Structure
```
├── docker/
│ ├── nginx/default.conf # Dev Nginx config
│ ├── php/
│ │ ├── Dockerfile # PHP-FPM image
│ │ └── local.ini # PHP dev settings
│ └── mysql/my.cnf # MySQL config
├── deploy/
│ ├── nginx/
│ │ ├── laravel-site.conf # Production Nginx config
│ │ └── nginx-proxy-manager-notes.md
│ ├── apache/
│ │ ├── laravel-site.conf # Apache virtual host
│ │ └── apache-setup.md
│ ├── production/
│ │ ├── .env.mysql.production # MySQL env template
│ │ ├── .env.pgsql.production # PostgreSQL env template
│ │ └── .env.sqlite.production # SQLite env template
│ └── scripts/
│ ├── server-setup.sh # Ubuntu server setup (DB selection)
│ ├── deploy.sh # Deployment script
│ ├── fix-permissions.sh # Permission fixer
│ ├── supervisor-worker.conf # Queue worker config
│ ├── supervisor-scheduler.conf # Scheduler config
│ └── laravel-scheduler.cron # Cron job template
├── src/
│ ├── .env.mysql # MySQL dev config
│ ├── .env.pgsql # PostgreSQL dev config
│ ├── .env.sqlite # SQLite dev config
│ └── pint.json # Code style config
├── scripts/
│ ├── post-install.sh # Flare/Telescope/Pint setup
│ └── laravel-setup.sh # Auth/API/Middleware setup
├── docs/
│ ├── error-logging.md # Error logging docs
│ ├── laravel-setup.md # Laravel setup guide
│ ├── filament-admin.md # Admin panel docs
│ ├── modules.md # Modular architecture guide
│ ├── module-generator.md # Admin UI module generator
│ ├── menu-management.md # Frontend menu system
│ ├── audit-trail.md # Audit trail docs
│ ├── site-settings.md # Appearance settings
│ ├── testing.md # Pest testing guide
│ ├── queues.md # Background jobs
│ ├── ci-cd.md # GitHub Actions pipeline
│ └── backup.md # Database backup/restore
├── docker-compose.yml # Multi-DB profiles
├── Makefile
├── README.md
├── GETTING_STARTED.md # Step-by-step setup guide
└── AI_CONTEXT.md # Context for AI assistants
```
## Database Options
This template supports three database engines via Docker Compose profiles:
| Database | Profile | Port | Use Case |
|----------|---------|------|----------|
| MySQL 8.0 | `mysql` | 3306 | Production-grade, most Laravel tutorials |
| PostgreSQL 16 | `pgsql` | 5432 | Advanced features, JSON, full-text search |
| SQLite | `sqlite` | - | Lightweight, no server, great for testing |
## Quick Start (Development)
### Prerequisites
- Docker & Docker Compose
### One-Command Setup
```bash
# Clone the repository
git clone <repo-url> my-laravel-app
cd my-laravel-app
# Run setup script (MySQL is default)
./setup.sh # Linux/Mac
setup.bat # Windows
# Or specify database:
./setup.sh mysql # MySQL (default)
./setup.sh pgsql # PostgreSQL
./setup.sh sqlite # SQLite
```
**That's it!** The script will:
- ✅ Configure environment for your database
- ✅ Install composer dependencies
- ✅ Build and start Docker containers
- ✅ Run migrations
- ✅ Create admin user (admin@example.com / password)
**Everything is pre-installed:**
- ✅ Laravel 11 with Breeze authentication
- ✅ Filament admin panel with user management
- ✅ Pest testing framework
- ✅ Laravel Pint code style
- ✅ Queue workers & scheduler (optional)
**Access your app:**
- Laravel App: http://localhost:8080
- Admin Panel: http://localhost:8080/admin
- Mailpit: http://localhost:8025
**Admin Login:**
Interactive (prompts for name and password)
php artisan make:admin admin@example.com
### Manual Setup (Alternative)
If you prefer manual control:
1. **Clone and configure**
```bash
git clone <repo-url> my-laravel-app
cd my-laravel-app
```
2. **Build containers**
```bash
docker-compose build
```
3. **Install Laravel**
```bash
docker-compose --profile mysql run --rm app composer create-project laravel/laravel:^11.0 /tmp/new
docker-compose --profile mysql run --rm app sh -c "cp -r /tmp/new/. /var/www/html/ && rm -rf /tmp/new"
```
4. **Configure environment**
```bash
cp src/.env.mysql src/.env # For MySQL
# OR
cp src/.env.pgsql src/.env # For PostgreSQL
# OR
cp src/.env.sqlite src/.env # For SQLite
```
5. **Start containers**
```bash
docker-compose --profile mysql up -d
```
6. **Run migrations**
```bash
docker-compose exec app php artisan migrate --force
```
7. **Run setup scripts (optional)**
```bash
docker-compose exec app bash scripts/laravel-setup.sh
```
### Common Commands
| Command | Description |
|---------|-------------|
| `make up DB=mysql` | Start with MySQL |
| `make up DB=pgsql` | Start with PostgreSQL |
| `make up DB=sqlite` | Start with SQLite |
| `make down` | Stop all containers |
| `make shell` | Shell into app container |
| `make artisan cmd='migrate'` | Run Artisan commands |
| `make composer cmd='require package'` | Run Composer |
| `make logs` | View logs |
| `make fresh` | Fresh migrate + seed |
| `make lint` | Fix code style (Pint) |
| `make lint-check` | Check code style |
| `make test` | Run tests |
| `make setup-tools` | Install Flare, Pint, error pages |
| `make setup-laravel` | Configure auth, API, middleware |
| `make setup-all` | Run both setup scripts |
### Frontend Assets (Vite)
Build frontend CSS/JS assets:
```bash
# Build assets for production
docker-compose run --rm node npm run build
# Or run Vite dev server (hot reload)
docker-compose --profile frontend up -d
```
> **Note:** The template includes a resilient fallback - if assets aren't built, basic styling is provided and a development warning is shown. This prevents the `ViteManifestNotFoundException` error from breaking the app.
## Laravel Setup (Auth, API, Middleware)
After installing Laravel, run the interactive setup:
```bash
make setup-laravel
```
This configures:
| Feature | Options |
|---------|---------|
| **Authentication** | Breeze (Blade/Livewire/API) or Jetstream + Livewire |
| **Admin Panel** | Filament with user management |
| **Site Settings** | Logo, favicon, color scheme management |
| **Modules** | Modular architecture with `make:module` command |
| **Audit Trail** | Track all data changes with user, old/new values |
| **Testing** | Pest framework with module test generation |
| **Queues** | Redis-powered background jobs |
| **CI/CD** | GitHub Actions for tests + deploy |
| **Backup** | Database backup/restore scripts |
| **API** | Sanctum token authentication |
| **Middleware** | ForceHttps, SecurityHeaders |
| **Storage** | Public storage symlink |
> **Note:** This template focuses on Blade and Livewire (no Vue/React/Inertia). Server-side rendering keeps debugging simple.
### Admin Panel (Filament)
The setup includes optional [Filament](https://filamentphp.com/) admin panel:
- **User management** - List, create, edit, delete users
- **Dashboard** - Stats widgets, charts
- **Extensible** - Add resources for any model
Access at: `http://localhost:8080/admin`
See [docs/filament-admin.md](docs/filament-admin.md) for customization.
### Site Settings (Appearance)
Manage logo, favicon, and colors from admin panel:
```
/admin → Settings → Appearance
```
Use in Blade templates:
```blade
{{-- In your <head> --}}
<x-site-head :title="$title" />
{{-- Logo --}}
<img src="{{ site_logo() }}" alt="{{ site_name() }}">
{{-- Colors available as CSS variables --}}
<div class="bg-primary">Uses --primary-color</div>
```
See [docs/site-settings.md](docs/site-settings.md) for configuration.
### Modular Architecture
Build features as self-contained modules:
```bash
# Create a module with model and admin panel
php artisan make:module StockManagement --model=Product
# Creates:
# - app/Modules/StockManagement/
# - Routes, controllers, views
# - Filament admin resources
# - Permissions for role-based access
```
Each module gets:
- **Landing page** at `/{module-slug}`
- **Admin section** in Filament panel
- **Permissions** auto-registered with roles
See [docs/modules.md](docs/modules.md) for full documentation.
### Audit Trail
Every module includes an **Audit Log** page showing all data changes:
- **Who** changed what
- **Old → New** values
- **When** and from which **IP**
- Filterable by user, event type, date
Configure per module in `Config/module_name.php`:
```php
'audit' => [
'enabled' => true,
'strategy' => 'all', // 'all', 'include', 'exclude', 'none'
],
```
See [docs/audit-trail.md](docs/audit-trail.md) for configuration options.
See [docs/laravel-setup.md](docs/laravel-setup.md) for detailed configuration.
## Production Deployment
### Ubuntu 24.04 Server Setup
1. **Run server setup script**
```bash
sudo bash deploy/scripts/server-setup.sh
```
This installs:
- PHP 8.3 + extensions (MySQL, PostgreSQL, SQLite drivers)
- Composer
- Node.js 20
- Database server (MySQL, PostgreSQL, or SQLite - your choice)
- Redis
- Nginx or Apache (your choice)
2. **Create database** (based on your selection during setup)
**MySQL:**
```bash
sudo mysql_secure_installation
sudo mysql
CREATE DATABASE your_app;
CREATE USER 'your_user'@'localhost' IDENTIFIED BY 'secure_password';
GRANT ALL PRIVILEGES ON your_app.* TO 'your_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
```
**PostgreSQL:**
```bash
sudo -u postgres psql
CREATE DATABASE your_app;
CREATE USER your_user WITH ENCRYPTED PASSWORD 'secure_password';
GRANT ALL PRIVILEGES ON DATABASE your_app TO your_user;
\q
```
**SQLite:**
```bash
touch /var/www/your-app/database/database.sqlite
chmod 664 /var/www/your-app/database/database.sqlite
chown www-data:www-data /var/www/your-app/database/database.sqlite
```
### Option A: Nginx + Nginx Proxy Manager
1. **Deploy your app**
```bash
cd /var/www
git clone <repo-url> your-app
cd your-app/src
composer install --no-dev --optimize-autoloader
# Copy the appropriate .env file for your database:
cp ../deploy/production/.env.mysql.production .env # For MySQL
cp ../deploy/production/.env.pgsql.production .env # For PostgreSQL
cp ../deploy/production/.env.sqlite.production .env # For SQLite
# Edit .env with your settings
php artisan key:generate
php artisan migrate
npm install && npm run build
php artisan db:seed
php artisan make:admin user@domain.com
```
2. **Configure Nginx**
```bash
sudo cp deploy/nginx/laravel-site.conf /etc/nginx/sites-available/your-app
# Edit: server_name, root, log paths
sudo ln -s /etc/nginx/sites-available/your-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
3. **Configure NPM**
- See `deploy/nginx/nginx-proxy-manager-notes.md` for NPM setup
4. **Fix permissions**
```bash
sudo bash deploy/scripts/fix-permissions.sh /var/www/your-app/src
```
### Option B: Apache Virtual Host
1. **Deploy your app** (same as above)
2. **Configure Apache**
```bash
sudo cp deploy/apache/laravel-site.conf /etc/apache2/sites-available/your-app.conf
# Edit: ServerName, DocumentRoot, paths
sudo a2ensite your-app.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
```
3. **SSL with Certbot**
```bash
sudo certbot --apache -d your-domain.com
```
See `deploy/apache/apache-setup.md` for detailed instructions.
### Automated Deployments
Use the deployment script for updates:
```bash
sudo bash deploy/scripts/deploy.sh /var/www/your-app/src main
```
### Queue Workers (Optional)
If using queues:
```bash
sudo cp deploy/scripts/supervisor-worker.conf /etc/supervisor/conf.d/laravel-worker.conf
# Edit paths in the config
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
```
## Services
### Development
| Service | Port | Description |
|---------|------|-------------|
| Nginx | 8080 | Web server |
| MySQL | 3306 | Database |
| Redis | 6379 | Cache/Queue |
| Mailpit | 8025 | Email testing UI |
| Mailpit SMTP | 1025 | SMTP server |
### Connecting to MySQL from host
```bash
mysql -h 127.0.0.1 -P 3306 -u laravel -p
# Password: secret (from .env)
```
## Error Logging
This template uses **Flare + Ignition** by Spatie for error tracking.
| Environment | Tool | What You Get |
|-------------|------|--------------|
| Development | Ignition | Rich error pages, AI explanations, click-to-open in VS Code |
| Production | Flare | Remote error tracking, clean user-facing error pages |
### Setup
After installing Laravel, run the post-install script:
```bash
make setup-tools
```
This installs:
- Flare for production error tracking
- Custom error pages (404, 500, 503)
- Optional: Laravel Telescope for debugging
Get your Flare API key at [flareapp.io](https://flareapp.io) and add to `.env`:
```env
FLARE_KEY=your_key_here
```
See [docs/error-logging.md](docs/error-logging.md) for full documentation.
## Code Style (Laravel Pint)
This template includes [Laravel Pint](https://laravel.com/docs/pint) for code style enforcement.
```bash
# Fix code style
make lint
# Check without fixing
make lint-check
```
Configuration: `src/pint.json` (uses Laravel preset with sensible defaults).
## Scheduler (Production)
Laravel's task scheduler needs to run every minute. Two options:
### Option 1: Cron (Recommended)
```bash
# Add to crontab
sudo crontab -e -u www-data
# Add this line:
* * * * * cd /var/www/your-app && php artisan schedule:run >> /dev/null 2>&1
```
### Option 2: Supervisor
```bash
sudo cp deploy/scripts/supervisor-scheduler.conf /etc/supervisor/conf.d/
# Edit paths in the config
sudo supervisorctl reread && sudo supervisorctl update
```
## Customization
### Changing PHP Version
Edit `docker/php/Dockerfile`:
```dockerfile
FROM php:8.2-fpm # Change version here
```
Then rebuild: `docker-compose build app`
### Adding PHP Extensions
Edit `docker/php/Dockerfile` and add to the install list, then rebuild.
### Using PostgreSQL
1. Uncomment PostgreSQL in `docker-compose.yml`
2. Update `src/.env`:
```
DB_CONNECTION=pgsql
DB_HOST=pgsql
```
## Troubleshooting
### Permission Issues
```bash
# Development
docker-compose exec app chmod -R 775 storage bootstrap/cache
# Production
sudo bash deploy/scripts/fix-permissions.sh /var/www/your-app/src
```
### Container won't start
```bash
docker-compose logs app # Check for errors
docker-compose down -v # Reset volumes
docker-compose up -d
```
### Database connection refused
- Ensure MySQL container is running: `docker-compose ps`
- Check `DB_HOST=mysql` in `src/.env` (not `localhost`)
## License
MIT

2
REPO_MANAGEMENT.md Normal file
View File

@@ -0,0 +1,2 @@
Commits must be againsts the module being currently worked on.
Commits must be to a branch that has the exact same name as the module being worked on.

View File

@@ -0,0 +1,16 @@
# Template Improvement Suggestions
This document contains suggestions for improving the template based on recurring development issues.
## 1. Mandatory Module Settings Page
**Rule**: When creating a new module, the AI agent must always create a corresponding settings page within the admin panel.
**Minimum Requirements**: This settings page must, at a minimum, allow an administrator to configure which user roles have Create, Read, Update, and Delete (CRUD) permissions for that module.
**Rationale**: This has been a recurring issue. Automating the creation of a permission management UI for each module makes the template more robust, secure, and user-friendly out-of-the-box. It prevents situations where new modules are added without any way for an admin to control access to them.
**Example Implementation**:
- When a `Blog` module is created, a `Blog Settings` page should also be created.
- This page should contain a form with checkboxes or a multi-select dropdown for each CRUD permission (`blog.view`, `blog.create`, `blog.edit`, `blog.delete`).
- An administrator can then select which roles (e.g., 'admin', 'editor', 'viewer') are granted each of these permissions.

420
TEST_SETUP.md Normal file
View File

@@ -0,0 +1,420 @@
# Testing the 2-Minute Setup
This document verifies that the Laravel Docker Dev Template works as advertised.
---
## Test 1: Fresh Local Setup (Docker)
### Prerequisites
- Docker Desktop running
- Git installed
- No existing containers from this project
### Steps
```bash
# 1. Clone to a fresh directory
cd /tmp # or C:\temp on Windows
git clone https://git.radapps.co.za/theradcoza/Laravel-Docker-Dev-Template.git test-setup
cd test-setup
# 2. Run setup (should take ~2 minutes)
./setup.sh mysql # or setup.bat mysql on Windows
# 3. Verify containers are running
docker-compose ps
```
**Expected Output:**
- `app` container: Up
- `nginx` container: Up
- `mysql` container: Up
- `redis` container: Up
- `mailpit` container: Up
### Verification Checklist
**Database:**
```bash
docker-compose exec app php artisan tinker
```
```php
// Check admin user exists
App\Models\User::where('email', 'admin@example.com')->exists(); // Should return: true
// Check roles exist
Spatie\Permission\Models\Role::count(); // Should return: 3
// Check permissions exist
Spatie\Permission\Models\Permission::count(); // Should return: 5
// Check admin has admin role
$admin = App\Models\User::where('email', 'admin@example.com')->first();
$admin->hasRole('admin'); // Should return: true
exit
```
**Web Access:**
1. Visit http://localhost:8080 - Should show Laravel welcome page
2. Visit http://localhost:8080/admin - Should show Filament login
3. Login with:
- Email: admin@example.com
- Password: password
4. Should successfully login and see admin dashboard
5. Should see "Users" menu item
6. Should see "Settings" menu item
**Settings Page:**
1. Click "Settings" in admin panel
2. Should load without errors
3. Should show form fields:
- Site Name
- Site Logo (file upload)
- Primary Color
- Secondary Color
- Accent Color
- Site Description
- Contact Email
- Maintenance Mode (toggle)
4. Try saving - should show success notification
**Users Management:**
1. Click "Users" in admin panel
2. Should see admin user in list
3. Click "New User"
4. Create test user
5. Should save successfully
**Mailpit:**
1. Visit http://localhost:8025
2. Should show Mailpit interface
3. Send a test email (e.g., password reset)
4. Email should appear in Mailpit
### Success Criteria
✅ Setup completes in under 3 minutes
✅ All containers start successfully
✅ Admin user exists with correct credentials
✅ 3 roles created (admin, editor, viewer)
✅ 5 permissions created
✅ Admin login works
✅ Settings page loads and saves
✅ Users management works
✅ No 500 errors
✅ No 419 CSRF errors
✅ No "Class Redis not found" errors
---
## Test 2: Production Deployment Simulation
### Prerequisites
- Ubuntu 24.04 server (or VM)
- Root or sudo access
- Clean database
### Steps
Follow `PRODUCTION_DEPLOYMENT.md` exactly:
```bash
# 1. Install PHP and extensions
sudo apt update
sudo apt install -y php8.3 php8.3-fpm php8.3-cli php8.3-mysql \
php8.3-redis php8.3-curl php8.3-mbstring php8.3-xml \
php8.3-zip php8.3-bcmath php8.3-gd
# 2. Verify Redis extension
php -m | grep redis # Should output: redis
# 3. Clone repo
cd /var/www
sudo git clone https://git.radapps.co.za/theradcoza/Laravel-Docker-Dev-Template.git test-app
cd test-app/src
# 4. Install dependencies
composer install --no-dev --optimize-autoloader
npm install && npm run build
# 5. Configure environment
cp .env.mysql .env
nano .env # Set database credentials, APP_URL, etc.
# 6. Generate key
php artisan key:generate --force
# 7. Run migrations
php artisan migrate --force
# 8. CRITICAL: Run seeders
php artisan db:seed --force
# 9. Set permissions
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
# 10. Create storage link
php artisan storage:link
# 11. Cache config
php artisan config:cache
php artisan route:cache
php artisan view:cache
```
### Verification
```bash
# Check admin user exists
php artisan tinker
```
```php
App\Models\User::where('email', 'admin@example.com')->exists(); // true
exit
```
**Configure web server** (Apache or Nginx) per `PRODUCTION_DEPLOYMENT.md`
**Test web access:**
1. Visit https://your-domain.com
2. Visit https://your-domain.com/admin
3. Login with admin@example.com / password
4. Should work without errors
### Success Criteria
✅ PHP Redis extension installed
✅ Migrations run successfully
✅ Seeders run successfully
✅ Admin user created
✅ Roles and permissions created
✅ No "Class Redis not found" errors
✅ No 419 CSRF errors
✅ Admin login works
✅ Settings page works
✅ No 500 errors
---
## Test 3: Common Error Scenarios
### Scenario 1: Missing Redis Extension
**Simulate:**
```bash
# On production server, DON'T install php-redis
composer install
php artisan migrate
```
**Expected:**
- Should fail with clear error message
- `PRODUCTION_DEPLOYMENT.md` should have solution
**Fix:**
```bash
sudo apt install php8.3-redis
sudo systemctl restart php8.3-fpm
```
### Scenario 2: Forgot to Run Seeders
**Simulate:**
```bash
php artisan migrate --force
# Skip: php artisan db:seed --force
```
**Expected:**
- Admin user doesn't exist
- Roles don't exist
- Can't login
**Fix:**
```bash
php artisan db:seed --force
```
### Scenario 3: Permission Issues
**Simulate:**
```bash
# Wrong permissions on storage
sudo chmod -R 755 storage
```
**Expected:**
- 500 errors
- Can't write logs
**Fix:**
```bash
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
```
---
## Automated Test Script
Create `test-setup.sh`:
```bash
#!/bin/bash
echo "🧪 Testing Laravel Docker Dev Template Setup..."
echo ""
# Test 1: Check containers
echo "✓ Checking containers..."
if ! docker-compose ps | grep -q "Up"; then
echo "❌ Containers not running"
exit 1
fi
echo "✅ Containers running"
# Test 2: Check admin user
echo "✓ Checking admin user..."
ADMIN_EXISTS=$(docker-compose exec -T app php artisan tinker --execute="echo App\Models\User::where('email', 'admin@example.com')->exists() ? 'true' : 'false';")
if [[ ! "$ADMIN_EXISTS" =~ "true" ]]; then
echo "❌ Admin user not found"
exit 1
fi
echo "✅ Admin user exists"
# Test 3: Check roles
echo "✓ Checking roles..."
ROLE_COUNT=$(docker-compose exec -T app php artisan tinker --execute="echo Spatie\Permission\Models\Role::count();")
if [[ ! "$ROLE_COUNT" =~ "3" ]]; then
echo "❌ Expected 3 roles, found: $ROLE_COUNT"
exit 1
fi
echo "✅ 3 roles created"
# Test 4: Check permissions
echo "✓ Checking permissions..."
PERM_COUNT=$(docker-compose exec -T app php artisan tinker --execute="echo Spatie\Permission\Models\Permission::count();")
if [[ ! "$PERM_COUNT" =~ "5" ]]; then
echo "❌ Expected 5 permissions, found: $PERM_COUNT"
exit 1
fi
echo "✅ 5 permissions created"
# Test 5: Check admin has admin role
echo "✓ Checking admin role assignment..."
HAS_ROLE=$(docker-compose exec -T app php artisan tinker --execute="echo App\Models\User::where('email', 'admin@example.com')->first()->hasRole('admin') ? 'true' : 'false';")
if [[ ! "$HAS_ROLE" =~ "true" ]]; then
echo "❌ Admin user doesn't have admin role"
exit 1
fi
echo "✅ Admin has admin role"
# Test 6: Check web access
echo "✓ Checking web access..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080)
if [ "$HTTP_CODE" != "200" ]; then
echo "❌ Web server not responding (HTTP $HTTP_CODE)"
exit 1
fi
echo "✅ Web server responding"
# Test 7: Check admin panel
echo "✓ Checking admin panel..."
ADMIN_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/admin)
if [ "$ADMIN_CODE" != "200" ]; then
echo "❌ Admin panel not responding (HTTP $ADMIN_CODE)"
exit 1
fi
echo "✅ Admin panel responding"
echo ""
echo "🎉 All tests passed!"
echo ""
echo "Admin Login:"
echo " URL: http://localhost:8080/admin"
echo " Email: admin@example.com"
echo " Password: password"
```
Make executable:
```bash
chmod +x test-setup.sh
```
Run:
```bash
./test-setup.sh
```
---
## Quick Test (30 seconds)
If you just want to verify the basics work:
```bash
# After running setup.sh
docker-compose exec app php artisan tinker --execute="
echo 'Admin exists: ' . (App\Models\User::where('email', 'admin@example.com')->exists() ? 'YES' : 'NO') . PHP_EOL;
echo 'Roles count: ' . Spatie\Permission\Models\Role::count() . PHP_EOL;
echo 'Permissions count: ' . Spatie\Permission\Models\Permission::count() . PHP_EOL;
echo 'Admin has role: ' . (App\Models\User::where('email', 'admin@example.com')->first()->hasRole('admin') ? 'YES' : 'NO') . PHP_EOL;
"
```
**Expected output:**
```
Admin exists: YES
Roles count: 3
Permissions count: 5
Admin has role: YES
```
Then visit http://localhost:8080/admin and login.
---
## Reporting Issues
If any test fails:
1. **Capture logs:**
```bash
docker-compose logs app > app-logs.txt
docker-compose exec app cat storage/logs/laravel.log > laravel-logs.txt
```
2. **Capture database state:**
```bash
docker-compose exec app php artisan tinker --execute="
echo 'Users: ' . App\Models\User::count() . PHP_EOL;
echo 'Roles: ' . Spatie\Permission\Models\Role::count() . PHP_EOL;
echo 'Permissions: ' . Spatie\Permission\Models\Permission::count() . PHP_EOL;
" > db-state.txt
```
3. **Report with:**
- Which test failed
- Error messages
- Log files
- Steps to reproduce
---
## Success Metrics
The template is considered "working correctly" if:
- ✅ Fresh setup completes in under 3 minutes
- ✅ Zero manual intervention required
- ✅ Admin user auto-created with correct credentials
- ✅ All roles and permissions seeded
- ✅ Admin panel accessible immediately
- ✅ Settings page works without errors
- ✅ No Redis errors
- ✅ No CSRF errors
- ✅ Production deployment follows clear guide
- ✅ All common errors documented with solutions

View File

@@ -0,0 +1,92 @@
# Apache Virtual Host Setup for Laravel
## Prerequisites
Ubuntu 24.04 with Apache2 and PHP-FPM installed.
## Required Apache Modules
Enable the necessary modules:
```bash
sudo a2enmod rewrite
sudo a2enmod headers
sudo a2enmod ssl
sudo a2enmod proxy_fcgi
sudo a2enmod deflate
sudo a2enmod expires
sudo a2enmod setenvif
```
## Installation Steps
### 1. Copy Virtual Host Configuration
```bash
sudo cp deploy/apache/laravel-site.conf /etc/apache2/sites-available/your-app.conf
```
### 2. Edit Configuration
Update the following in the config file:
- `ServerName` - Your domain name
- `DocumentRoot` - Path to Laravel's public folder
- `Directory` - Same path as DocumentRoot
- Log file names
### 3. Enable Site
```bash
sudo a2ensite your-app.conf
sudo a2dissite 000-default.conf # Disable default site if needed
```
### 4. Test Configuration
```bash
sudo apache2ctl configtest
```
### 5. Restart Apache
```bash
sudo systemctl restart apache2
```
## SSL with Certbot
Install Certbot and obtain SSL certificate:
```bash
sudo apt install certbot python3-certbot-apache
sudo certbot --apache -d your-domain.com -d www.your-domain.com
```
Certbot will automatically modify your Apache configuration for SSL.
## File Permissions
Set correct permissions for Laravel:
```bash
sudo chown -R www-data:www-data /var/www/your-app
sudo chmod -R 755 /var/www/your-app
sudo chmod -R 775 /var/www/your-app/storage
sudo chmod -R 775 /var/www/your-app/bootstrap/cache
```
## Common Issues
### 403 Forbidden
- Check directory permissions
- Ensure `AllowOverride All` is set
- Verify `mod_rewrite` is enabled
### 500 Internal Server Error
- Check Laravel logs: `storage/logs/laravel.log`
- Check Apache error logs: `/var/log/apache2/your-app-error.log`
- Ensure `.env` file exists and has correct permissions
### PHP Not Processing
- Verify PHP-FPM is running: `sudo systemctl status php8.3-fpm`
- Check socket path matches in Apache config

View File

@@ -0,0 +1,115 @@
<VirtualHost *:80>
ServerName your-domain.com
ServerAlias www.your-domain.com
ServerAdmin webmaster@your-domain.com
DocumentRoot /var/www/your-app/public
<Directory /var/www/your-app/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# PHP-FPM configuration
<FilesMatch \.php$>
SetHandler "proxy:unix:/var/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>
# Security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Disable server signature
ServerSignature Off
# Logging
ErrorLog ${APACHE_LOG_DIR}/your-app-error.log
CustomLog ${APACHE_LOG_DIR}/your-app-access.log combined
# Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
AddOutputFilterByType DEFLATE application/javascript application/json
AddOutputFilterByType DEFLATE application/xml application/xhtml+xml
AddOutputFilterByType DEFLATE image/svg+xml
</IfModule>
# Static file caching
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType font/woff2 "access plus 1 month"
ExpiresByType font/woff "access plus 1 month"
</IfModule>
</VirtualHost>
# SSL Configuration (use with Let's Encrypt / Certbot)
<VirtualHost *:443>
ServerName your-domain.com
ServerAlias www.your-domain.com
ServerAdmin webmaster@your-domain.com
DocumentRoot /var/www/your-app/public
<Directory /var/www/your-app/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# PHP-FPM configuration
<FilesMatch \.php$>
SetHandler "proxy:unix:/var/run/php/php8.3-fpm.sock|fcgi://localhost"
</FilesMatch>
# SSL Configuration (Certbot will fill these in)
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/your-domain.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/your-domain.com/privkey.pem
# Modern SSL configuration
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
# Security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
ServerSignature Off
ErrorLog ${APACHE_LOG_DIR}/your-app-ssl-error.log
CustomLog ${APACHE_LOG_DIR}/your-app-ssl-access.log combined
# Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
AddOutputFilterByType DEFLATE application/javascript application/json
AddOutputFilterByType DEFLATE application/xml application/xhtml+xml
AddOutputFilterByType DEFLATE image/svg+xml
</IfModule>
# Static file caching
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType font/woff2 "access plus 1 month"
ExpiresByType font/woff "access plus 1 month"
</IfModule>
</VirtualHost>

View File

@@ -0,0 +1,77 @@
# Nginx site configuration for Laravel on Ubuntu 24.04
# This config is for native Nginx (not Docker) behind Nginx Proxy Manager
# Place this in /etc/nginx/sites-available/ and symlink to sites-enabled
server {
listen 80;
listen [::]:80;
# Replace with your domain or internal IP
# Nginx Proxy Manager will forward traffic here
server_name your-domain.com;
root /var/www/your-app/public;
index index.php index.html index.htm;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
charset utf-8;
# Laravel routing
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Favicon and robots
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
# Error pages
error_page 404 /index.php;
# PHP-FPM configuration
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
# Timeouts
fastcgi_connect_timeout 60;
fastcgi_send_timeout 180;
fastcgi_read_timeout 180;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
}
# Deny access to hidden files
location ~ /\.(?!well-known).* {
deny all;
}
# Static file caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|pdf|txt|woff|woff2|ttf|svg)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml application/xml+rss application/x-font-ttf font/opentype image/svg+xml;
client_max_body_size 100M;
# Logging
access_log /var/log/nginx/your-app-access.log;
error_log /var/log/nginx/your-app-error.log;
}

View File

@@ -0,0 +1,79 @@
# Nginx Proxy Manager Configuration Notes
## Architecture Overview
```
Internet → Nginx Proxy Manager (Docker/Host) → Native Nginx → PHP-FPM → Laravel
SSL Termination
(Let's Encrypt)
```
## Nginx Proxy Manager Setup
### Option 1: NPM on Same Server
If running NPM on the same Ubuntu 24.04 server:
1. **NPM listens on ports 80/443** (public)
2. **Native Nginx listens on port 8080** (internal only)
3. NPM forwards traffic to `localhost:8080`
Modify `laravel-site.conf`:
```nginx
server {
listen 127.0.0.1:8080; # Only accept local connections
...
}
```
### Option 2: NPM on Separate Server
If running NPM on a separate server:
1. Configure firewall to allow NPM server IP
2. NPM forwards to `http://your-laravel-server-ip:80`
## NPM Proxy Host Configuration
In Nginx Proxy Manager web UI:
1. **Domain Names**: your-domain.com
2. **Scheme**: http
3. **Forward Hostname/IP**: 127.0.0.1 (or server IP)
4. **Forward Port**: 8080 (or 80)
5. **Enable**: Block Common Exploits
6. **SSL Tab**:
- Request new SSL Certificate
- Force SSL
- HTTP/2 Support
## Custom NPM Configuration
Add to "Advanced" tab if needed:
```nginx
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
# WebSocket support (if using Laravel Echo/Reverb)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
```
## Laravel Trusted Proxies
Update `app/Http/Middleware/TrustProxies.php` or configure in Laravel 11+:
```php
// In bootstrap/app.php or config
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: '*');
})
```
Or set in `.env`:
```
TRUSTED_PROXIES=*
```

View File

@@ -0,0 +1,62 @@
APP_NAME="Your App Name"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_TIMEZONE=UTC
APP_URL=https://your-domain.com
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
APP_MAINTENANCE_STORE=database
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=error
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=your_db_user
DB_PASSWORD=your_secure_password
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_ENCRYPT=true
SESSION_PATH=/
SESSION_DOMAIN=your-domain.com
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis
CACHE_STORE=redis
CACHE_PREFIX=
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=smtp.your-provider.com
MAIL_PORT=587
MAIL_USERNAME=your_email_username
MAIL_PASSWORD=your_email_password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="noreply@your-domain.com"
MAIL_FROM_NAME="${APP_NAME}"
TRUSTED_PROXIES=*
VITE_APP_NAME="${APP_NAME}"
# Error Logging - Flare (https://flareapp.io)
# REQUIRED for production error tracking
FLARE_KEY=your_flare_key_here

View File

@@ -0,0 +1,62 @@
APP_NAME="Your App Name"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_TIMEZONE=UTC
APP_URL=https://your-domain.com
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
APP_MAINTENANCE_STORE=database
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=error
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=your_database
DB_USERNAME=your_db_user
DB_PASSWORD=your_secure_password
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_ENCRYPT=true
SESSION_PATH=/
SESSION_DOMAIN=your-domain.com
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis
CACHE_STORE=redis
CACHE_PREFIX=
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=smtp.your-provider.com
MAIL_PORT=587
MAIL_USERNAME=your_email_username
MAIL_PASSWORD=your_email_password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="noreply@your-domain.com"
MAIL_FROM_NAME="${APP_NAME}"
TRUSTED_PROXIES=*
VITE_APP_NAME="${APP_NAME}"
# Error Logging - Flare (https://flareapp.io)
# REQUIRED for production error tracking
FLARE_KEY=your_flare_key_here

View File

@@ -0,0 +1,58 @@
APP_NAME="Your App Name"
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_TIMEZONE=UTC
APP_URL=https://your-domain.com
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
APP_MAINTENANCE_STORE=database
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=error
DB_CONNECTION=sqlite
DB_DATABASE=/var/www/your-app/database/database.sqlite
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_ENCRYPT=true
SESSION_PATH=/
SESSION_DOMAIN=your-domain.com
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis
CACHE_STORE=redis
CACHE_PREFIX=
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=smtp.your-provider.com
MAIL_PORT=587
MAIL_USERNAME=your_email_username
MAIL_PASSWORD=your_email_password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="noreply@your-domain.com"
MAIL_FROM_NAME="${APP_NAME}"
TRUSTED_PROXIES=*
VITE_APP_NAME="${APP_NAME}"
# Error Logging - Flare (https://flareapp.io)
# REQUIRED for production error tracking
FLARE_KEY=your_flare_key_here

75
deploy/scripts/deploy.sh Normal file
View File

@@ -0,0 +1,75 @@
#!/bin/bash
# Laravel Deployment Script
# Usage: ./deploy.sh /var/www/your-app [branch]
set -e
APP_PATH="${1:-/var/www/laravel}"
BRANCH="${2:-main}"
if [ ! -d "$APP_PATH" ]; then
echo "Error: Directory $APP_PATH does not exist"
exit 1
fi
cd "$APP_PATH"
echo "=========================================="
echo "Deploying Laravel Application"
echo "Path: $APP_PATH"
echo "Branch: $BRANCH"
echo "=========================================="
# Enable maintenance mode
echo "[1/9] Enabling maintenance mode..."
php artisan down --retry=60 || true
# Pull latest code
echo "[2/9] Pulling latest changes..."
git fetch origin
git reset --hard origin/$BRANCH
# Install PHP dependencies
echo "[3/9] Installing Composer dependencies..."
composer install --no-dev --optimize-autoloader --no-interaction
# Install and build frontend assets
echo "[4/9] Installing Node dependencies..."
npm install
echo "[5/9] Building frontend assets..."
npm run build
# Run migrations
echo "[6/9] Running database migrations..."
php artisan migrate --force
# Clear and optimize
echo "[7/9] Optimizing application..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Fix permissions
echo "[8/9] Fixing permissions..."
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R 775 storage bootstrap/cache
# Restart services
echo "[9/9] Restarting services..."
sudo systemctl restart php8.3-fpm
# Restart queue workers if using Supervisor
if [ -f /etc/supervisor/conf.d/laravel-worker.conf ]; then
sudo supervisorctl restart laravel-worker:*
fi
# Disable maintenance mode
php artisan up
echo ""
echo "=========================================="
echo "Deployment complete!"
echo "=========================================="

View File

@@ -0,0 +1,43 @@
#!/bin/bash
# Fix Laravel file permissions
# Usage: ./fix-permissions.sh /var/www/your-app
set -e
if [ -z "$1" ]; then
echo "Usage: $0 /path/to/laravel/app"
exit 1
fi
APP_PATH="$1"
if [ ! -d "$APP_PATH" ]; then
echo "Error: Directory $APP_PATH does not exist"
exit 1
fi
echo "Fixing permissions for: $APP_PATH"
# Set ownership
sudo chown -R www-data:www-data "$APP_PATH"
# Set directory permissions
sudo find "$APP_PATH" -type d -exec chmod 755 {} \;
# Set file permissions
sudo find "$APP_PATH" -type f -exec chmod 644 {} \;
# Make storage and cache writable
sudo chmod -R 775 "$APP_PATH/storage"
sudo chmod -R 775 "$APP_PATH/bootstrap/cache"
# Set ACL for current user to maintain access
if command -v setfacl &> /dev/null; then
sudo setfacl -Rm u:$(whoami):rwx "$APP_PATH/storage"
sudo setfacl -Rm u:$(whoami):rwx "$APP_PATH/bootstrap/cache"
sudo setfacl -dRm u:$(whoami):rwx "$APP_PATH/storage"
sudo setfacl -dRm u:$(whoami):rwx "$APP_PATH/bootstrap/cache"
fi
echo "Permissions fixed successfully!"

View File

@@ -0,0 +1,6 @@
# Laravel Scheduler Cron Job
# Add this to crontab: sudo crontab -e -u www-data
# Or copy to /etc/cron.d/laravel-scheduler
# Run Laravel scheduler every minute
* * * * * www-data cd /var/www/your-app && php artisan schedule:run >> /dev/null 2>&1

View File

@@ -0,0 +1,202 @@
#!/bin/bash
# Laravel Server Setup Script for Ubuntu 24.04
# Run as root or with sudo
set -e
echo "=========================================="
echo "Laravel Server Setup for Ubuntu 24.04"
echo "=========================================="
# Update system
echo "[1/8] Updating system packages..."
apt update && apt upgrade -y
# Install essential packages
echo "[2/8] Installing essential packages..."
apt install -y \
software-properties-common \
curl \
git \
unzip \
supervisor \
acl
# Add PHP repository and install PHP 8.3
echo "[3/8] Installing PHP 8.3 and extensions..."
add-apt-repository -y ppa:ondrej/php
apt update
apt install -y \
php8.3-fpm \
php8.3-cli \
php8.3-mysql \
php8.3-pgsql \
php8.3-sqlite3 \
php8.3-redis \
php8.3-mbstring \
php8.3-xml \
php8.3-curl \
php8.3-zip \
php8.3-bcmath \
php8.3-intl \
php8.3-gd \
php8.3-imagick \
php8.3-opcache
# Install Composer
echo "[4/8] Installing Composer..."
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# Install Node.js 20.x
echo "[5/8] Installing Node.js 20.x..."
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
# Database selection
echo "[6/8] Database Installation..."
echo ""
echo "Select database server to install:"
echo "1) MySQL 8.0"
echo "2) PostgreSQL 16"
echo "3) SQLite only (no server needed)"
echo "4) Both MySQL and PostgreSQL"
read -p "Enter choice [1-4]: " DB_CHOICE
case $DB_CHOICE in
1)
apt install -y mysql-server
systemctl enable mysql
systemctl start mysql
echo "MySQL 8.0 installed."
echo ""
echo "To secure MySQL, run: sudo mysql_secure_installation"
echo "To create database:"
echo " sudo mysql"
echo " CREATE DATABASE your_app;"
echo " CREATE USER 'your_user'@'localhost' IDENTIFIED BY 'password';"
echo " GRANT ALL PRIVILEGES ON your_app.* TO 'your_user'@'localhost';"
echo " FLUSH PRIVILEGES;"
;;
2)
apt install -y postgresql postgresql-contrib
systemctl enable postgresql
systemctl start postgresql
echo "PostgreSQL 16 installed."
echo ""
echo "To create database:"
echo " sudo -u postgres psql"
echo " CREATE DATABASE your_app;"
echo " CREATE USER your_user WITH ENCRYPTED PASSWORD 'password';"
echo " GRANT ALL PRIVILEGES ON DATABASE your_app TO your_user;"
;;
3)
echo "SQLite selected - no server installation needed."
echo "SQLite is included with PHP (php8.3-sqlite3 already installed)."
;;
4)
apt install -y mysql-server postgresql postgresql-contrib
systemctl enable mysql postgresql
systemctl start mysql postgresql
echo "Both MySQL and PostgreSQL installed."
;;
esac
# Install Redis
echo "[7/8] Installing Redis..."
apt install -y redis-server
systemctl enable redis-server
systemctl start redis-server
# Web server selection
echo "[8/8] Web Server Installation..."
echo ""
echo "Select web server to install:"
echo "1) Nginx (recommended for Nginx Proxy Manager setup)"
echo "2) Apache"
echo "3) Both"
echo "4) Skip (install manually later)"
read -p "Enter choice [1-4]: " WEB_SERVER_CHOICE
case $WEB_SERVER_CHOICE in
1)
apt install -y nginx
systemctl enable nginx
systemctl start nginx
echo "Nginx installed."
;;
2)
apt install -y apache2 libapache2-mod-fcgid
a2enmod rewrite headers ssl proxy_fcgi deflate expires setenvif
systemctl enable apache2
systemctl start apache2
echo "Apache installed with required modules."
;;
3)
apt install -y nginx apache2 libapache2-mod-fcgid
a2enmod rewrite headers ssl proxy_fcgi deflate expires setenvif
systemctl enable nginx apache2
# Stop Apache by default to avoid port conflict
systemctl stop apache2
systemctl start nginx
echo "Both installed. Nginx is running. Apache is stopped (start manually when needed)."
;;
4)
echo "Skipping web server installation."
;;
esac
# Configure PHP-FPM
echo ""
echo "Configuring PHP-FPM..."
cat > /etc/php/8.3/fpm/conf.d/99-laravel.ini << 'EOF'
upload_max_filesize = 100M
post_max_size = 100M
max_execution_time = 300
memory_limit = 512M
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 10000
opcache.validate_timestamps = 0
EOF
systemctl restart php8.3-fpm
# Create web directory
echo ""
echo "Creating web directory structure..."
mkdir -p /var/www
chown -R www-data:www-data /var/www
# Setup firewall
echo ""
echo "Configuring UFW firewall..."
ufw allow OpenSSH
ufw allow 'Nginx Full' 2>/dev/null || true
ufw allow 'Apache Full' 2>/dev/null || true
ufw --force enable
echo ""
echo "=========================================="
echo "Server setup complete!"
echo "=========================================="
echo ""
echo "Installed:"
echo " - PHP 8.3 with FPM and extensions (MySQL, PostgreSQL, SQLite drivers)"
echo " - Composer"
echo " - Node.js 20.x"
echo " - Database server (based on selection)"
echo " - Redis"
echo " - Web server (based on selection)"
echo ""
echo "Next steps:"
echo " 1. Create database and user for your selected database"
echo " 2. Clone your Laravel app to /var/www/your-app"
echo " 3. Copy appropriate .env file from deploy/production/"
echo " - .env.mysql.production"
echo " - .env.pgsql.production"
echo " - .env.sqlite.production"
echo " 4. Configure web server (use configs from deploy/nginx or deploy/apache)"
echo " 5. Set permissions: deploy/scripts/fix-permissions.sh"
echo ""

View File

@@ -0,0 +1,14 @@
# Laravel Scheduler via Supervisor (alternative to cron)
# Copy to: /etc/supervisor/conf.d/laravel-scheduler.conf
# This runs the scheduler in a loop instead of using cron
[program:laravel-scheduler]
process_name=%(program_name)s
command=/bin/bash -c "while [ true ]; do php /var/www/your-app/artisan schedule:run --verbose --no-interaction >> /var/www/your-app/storage/logs/scheduler.log 2>&1; sleep 60; done"
autostart=true
autorestart=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/var/www/your-app/storage/logs/supervisor-scheduler.log
stopwaitsecs=60

View File

@@ -0,0 +1,12 @@
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/your-app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/your-app/storage/logs/worker.log
stopwaitsecs=3600

167
docker-compose.yml Normal file
View File

@@ -0,0 +1,167 @@
services:
# PHP-FPM Application Server
app:
build:
context: .
dockerfile: docker/php/Dockerfile
restart: unless-stopped
working_dir: /var/www/html
volumes:
- ./src:/var/www/html:cached
- vendor_data:/var/www/html/vendor
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
networks:
- laravel_network
depends_on:
- redis
# Nginx Web Server
nginx:
image: nginx:alpine
restart: unless-stopped
ports:
- "${APP_PORT:-8080}:80"
volumes:
- ./src:/var/www/html:cached
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
networks:
- laravel_network
- gateway
depends_on:
- app
# ============================================
# DATABASE OPTIONS (use profiles to select)
# Start with: docker-compose --profile mysql up
# Or: docker-compose --profile pgsql up
# Or: docker-compose --profile sqlite up
# ============================================
# MySQL Database
mysql:
image: mysql:8.0
restart: unless-stopped
ports:
- "${DB_PORT:-3306}:3306"
environment:
MYSQL_DATABASE: ${DB_DATABASE:-laravel}
MYSQL_USER: ${DB_USERNAME:-laravel}
MYSQL_PASSWORD: ${DB_PASSWORD:-secret}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-rootsecret}
volumes:
- mysql_data:/var/lib/mysql
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
networks:
- laravel_network
profiles:
- mysql
# PostgreSQL Database
pgsql:
image: postgres:16-alpine
restart: unless-stopped
ports:
- "${DB_PORT:-5432}:5432"
environment:
POSTGRES_DB: ${DB_DATABASE:-laravel}
POSTGRES_USER: ${DB_USERNAME:-laravel}
POSTGRES_PASSWORD: ${DB_PASSWORD:-secret}
volumes:
- pgsql_data:/var/lib/postgresql/data
networks:
- laravel_network
profiles:
- pgsql
# SQLite (no container needed, just volume for persistence)
# SQLite runs inside the app container
# This is a dummy service to enable the sqlite profile
sqlite:
image: alpine:latest
volumes:
- ./src/database:/data
command: sh -c "touch /data/database.sqlite && chmod 666 /data/database.sqlite"
profiles:
- sqlite
# Redis Cache
redis:
image: redis:alpine
restart: unless-stopped
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
- redis_data:/data
networks:
- laravel_network
# Node.js for frontend assets (Vite/Mix)
node:
image: node:20-alpine
working_dir: /var/www/html
volumes:
- ./src:/var/www/html
networks:
- laravel_network
command: sh -c "npm install && npm run dev"
profiles:
- frontend
# Queue Worker (Laravel Horizon alternative for dev)
queue:
build:
context: .
dockerfile: docker/php/Dockerfile
restart: unless-stopped
working_dir: /var/www/html
volumes:
- ./src:/var/www/html
- vendor_data:/var/www/html/vendor
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
networks:
- laravel_network
depends_on:
- redis
command: php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
profiles:
- queue
# Scheduler (runs Laravel scheduler every minute)
scheduler:
build:
context: .
dockerfile: docker/php/Dockerfile
restart: unless-stopped
working_dir: /var/www/html
volumes:
- ./src:/var/www/html
- vendor_data:/var/www/html/vendor
- ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini
networks:
- laravel_network
command: sh -c "while true; do php artisan schedule:run --verbose --no-interaction & sleep 60; done"
profiles:
- scheduler
# Mailpit for local email testing
mailpit:
image: axllent/mailpit
restart: unless-stopped
ports:
- "${MAIL_PORT:-1025}:1025"
- "${MAIL_DASHBOARD_PORT:-8025}:8025"
networks:
- laravel_network
networks:
laravel_network:
driver: bridge
gateway:
external: true
volumes:
mysql_data:
pgsql_data:
redis_data:
vendor_data:

6
docker/.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
src/node_modules
src/vendor
.git
.env
*.log

9
docker/mysql/my.cnf Normal file
View File

@@ -0,0 +1,9 @@
[mysqld]
general_log = 1
general_log_file = /var/lib/mysql/general.log
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
default-authentication-plugin = mysql_native_password
[client]
default-character-set = utf8mb4

42
docker/nginx/default.conf Normal file
View File

@@ -0,0 +1,42 @@
server {
listen 80;
server_name localhost;
root /var/www/html/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript;
gzip_disable "MSIE [1-6]\.";
client_max_body_size 100M;
}

83
docker/php/Dockerfile Normal file
View File

@@ -0,0 +1,83 @@
FROM php:8.3-fpm
# Set working directory
WORKDIR /var/www/html
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
curl \
libpng-dev \
libonig-dev \
libxml2-dev \
libzip-dev \
libpq-dev \
libicu-dev \
zip \
unzip \
supervisor \
&& rm -rf /var/lib/apt/lists/*
# Install PHP extensions
RUN docker-php-ext-configure intl \
&& docker-php-ext-install \
pdo_mysql \
pdo_pgsql \
mbstring \
exif \
pcntl \
bcmath \
gd \
zip \
intl \
opcache
# Install Redis extension
RUN pecl install redis && docker-php-ext-enable redis
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Install Node.js for asset building
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# Create system user to run Composer and Artisan commands
RUN useradd -G www-data,root -u 1000 -d /home/devuser devuser
RUN mkdir -p /home/devuser/.composer && \
chown -R devuser:devuser /home/devuser
# Copy composer files first
COPY --chown=devuser:devuser ./src/composer.json ./src/composer.lock /var/www/html/
# Install composer dependencies as devuser
USER devuser
RUN composer install --no-interaction --no-dev --optimize-autoloader --no-scripts
# Switch back to root to copy rest of files
USER root
# Copy existing application directory
COPY --chown=devuser:devuser ./src /var/www/html
# Set proper permissions
RUN chown -R devuser:www-data /var/www/html \
&& chmod -R 775 /var/www/html/storage 2>/dev/null || true \
&& chmod -R 775 /var/www/html/bootstrap/cache 2>/dev/null || true
# Build frontend assets (if package.json exists)
RUN if [ -f "package.json" ]; then \
npm ci --ignore-scripts && \
npm run build && \
rm -rf node_modules; \
fi
# Run post-install scripts
USER devuser
RUN composer run-script post-autoload-dump 2>/dev/null || true
USER devuser
EXPOSE 9000
CMD ["php-fpm"]

21
docker/php/local.ini Normal file
View File

@@ -0,0 +1,21 @@
; PHP Configuration for Development
upload_max_filesize = 100M
post_max_size = 100M
max_execution_time = 600
memory_limit = 512M
; Error reporting
display_errors = On
display_startup_errors = On
error_reporting = E_ALL
; Opcache settings - ENABLED for performance (even in dev on Windows/WSL2)
opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 1
opcache.revalidate_freq = 2
opcache.fast_shutdown = 1

286
docs/audit-trail.md Normal file
View File

@@ -0,0 +1,286 @@
# Audit Trail
This template includes a comprehensive audit trail system using [owen-it/laravel-auditing](https://github.com/owen-it/laravel-auditing) with Filament UI integration.
## What Gets Tracked
For every audited model:
- **Who** - User who made the change
- **What** - Model and record ID
- **When** - Timestamp
- **Changes** - Old values → New values
- **Where** - IP address, user agent
- **Module** - Which module the change belongs to
## Quick Start
Audit trail is set up during `make setup-laravel`. To add auditing to a model:
```php
use App\Traits\ModuleAuditable;
use OwenIt\Auditing\Contracts\Auditable;
class Product extends Model implements Auditable
{
use ModuleAuditable;
// Your model code...
}
```
That's it! All create, update, and delete operations are now logged.
## Viewing Audit Logs
Each module has an **Audit Log** page in its admin section:
```
📦 Stock Management
├── Dashboard
├── Products
└── Audit Log ← Click here
```
The audit log shows:
- Date/Time
- User
- Event type (created/updated/deleted)
- Model
- Old → New values
Click any entry to see full details including IP address and all changed fields.
## Configuration
### Per-Module Configuration
Each module has audit settings in its config file:
```php
// Config/stock_management.php
'audit' => [
'enabled' => true, // Enable/disable for entire module
'strategy' => 'all', // 'all', 'include', 'exclude', 'none'
'exclude' => [ // Fields to never audit
'password',
'remember_token',
],
],
```
### Strategies
| Strategy | Description |
|----------|-------------|
| `all` | Audit all fields (default) |
| `include` | Only audit fields in `$auditInclude` |
| `exclude` | Audit all except fields in `$auditExclude` |
| `none` | Disable auditing |
### Per-Model Configuration
Override in your model:
```php
class Product extends Model implements Auditable
{
use ModuleAuditable;
// Only audit these fields
protected $auditInclude = [
'name',
'price',
'quantity',
];
// Or exclude specific fields
protected $auditExclude = [
'internal_notes',
'cache_data',
];
}
```
## Audit Events
By default, these events are tracked:
- `created` - New record created
- `updated` - Record modified
- `deleted` - Record deleted
### Custom Events
Log custom events:
```php
// In your model or service
$product->auditEvent = 'approved';
$product->isCustomEvent = true;
$product->auditCustomOld = ['status' => 'pending'];
$product->auditCustomNew = ['status' => 'approved'];
$product->save();
```
## Querying Audits
### Get audits for a record
```php
$product = Product::find(1);
$audits = $product->audits;
// With user info
$audits = $product->audits()->with('user')->get();
```
### Get audits by user
```php
use OwenIt\Auditing\Models\Audit;
$userAudits = Audit::where('user_id', $userId)->get();
```
### Get audits by module
```php
$moduleAudits = Audit::where('tags', 'like', '%module:StockManagement%')->get();
```
### Get recent changes
```php
$recentChanges = Audit::latest()->take(50)->get();
```
## UI Customization
### Modify Audit Log Table
Edit `Filament/Resources/AuditLogResource.php` in your module:
```php
public static function table(Table $table): Table
{
return $table
->columns([
// Add or modify columns
Tables\Columns\TextColumn::make('custom_field'),
])
->filters([
// Add custom filters
Tables\Filters\Filter::make('today')
->query(fn ($query) => $query->whereDate('created_at', today())),
]);
}
```
### Add Audit Tab to Resource
Add audit history tab to any Filament resource:
```php
use Tapp\FilamentAuditing\RelationManagers\AuditsRelationManager;
class ProductResource extends Resource
{
public static function getRelations(): array
{
return [
AuditsRelationManager::class,
];
}
}
```
## Data Retention
### Pruning Old Audits
Add to `app/Console/Kernel.php`:
```php
protected function schedule(Schedule $schedule)
{
// Delete audits older than 90 days
$schedule->command('audit:prune --days=90')->daily();
}
```
Or run manually:
```bash
php artisan audit:prune --days=90
```
### Archive Before Delete
```php
// Export to CSV before pruning
Audit::where('created_at', '<', now()->subDays(90))
->each(function ($audit) {
// Write to archive file
Storage::append('audits/archive.csv', $audit->toJson());
});
```
## Disabling Auditing
### Temporarily
```php
// Disable for a single operation
$product->disableAuditing();
$product->update(['price' => 99.99]);
$product->enableAuditing();
// Or use without auditing
Product::withoutAuditing(function () {
Product::where('category', 'sale')->update(['on_sale' => true]);
});
```
### For Specific Model
```php
class CacheModel extends Model implements Auditable
{
use ModuleAuditable;
// Disable auditing for this model
public $auditEvents = [];
}
```
### For Entire Module
```php
// Config/module_name.php
'audit' => [
'enabled' => false,
],
```
## Troubleshooting
### Audits not being created
1. Model implements `Auditable` interface
2. Model uses `ModuleAuditable` trait
3. Check module config `audit.enabled` is true
4. Run `php artisan config:clear`
### User not being recorded
1. Ensure user is authenticated when changes are made
2. Check `config/audit.php` for user resolver settings
### Performance concerns
1. Use `$auditInclude` to limit tracked fields
2. Set up audit pruning for old records
3. Consider async audit processing for high-volume apps
## Security
- Audit records are **read-only** in the admin panel
- No create/edit/delete actions available
- Access controlled by module permissions
- Sensitive fields (password, tokens) excluded by default

238
docs/backup.md Normal file
View File

@@ -0,0 +1,238 @@
# Database Backup & Restore
Scripts for backing up and restoring your database.
## Quick Start
```bash
# Create backup
make backup
# List backups
ls -la backups/
# Restore from backup
make restore file=backups/laravel_20240306_120000.sql.gz
```
## Backup
The backup script automatically:
- Detects database type (MySQL, PostgreSQL, SQLite)
- Creates timestamped backup
- Compresses with gzip
- Keeps only last 10 backups
### Manual Backup
```bash
./scripts/backup.sh
```
Output:
```
==========================================
Database Backup
==========================================
Connection: mysql
Database: laravel
Creating MySQL backup...
✓ Backup created successfully!
File: backups/laravel_20240306_120000.sql.gz
Size: 2.5M
Recent backups:
-rw-r--r-- 1 user user 2.5M Mar 6 12:00 laravel_20240306_120000.sql.gz
-rw-r--r-- 1 user user 2.4M Mar 5 12:00 laravel_20240305_120000.sql.gz
```
### Backup Location
```
backups/
├── laravel_20240306_120000.sql.gz
├── laravel_20240305_120000.sql.gz
└── laravel_20240304_120000.sql.gz
```
## Restore
```bash
# With make
make restore file=backups/laravel_20240306_120000.sql.gz
# Or directly
./scripts/restore.sh backups/laravel_20240306_120000.sql.gz
```
**Warning:** Restore will overwrite the current database!
## Automated Backups
### Using Scheduler
Add to your Laravel scheduler (`routes/console.php`):
```php
use Illuminate\Support\Facades\Schedule;
// Daily backup at 2 AM
Schedule::exec('bash scripts/backup.sh')
->dailyAt('02:00')
->sendOutputTo(storage_path('logs/backup.log'));
```
### Using Cron (Production)
```bash
# Edit crontab
crontab -e
# Add daily backup at 2 AM
0 2 * * * cd /var/www/html && bash scripts/backup.sh >> /var/log/laravel-backup.log 2>&1
```
## Remote Backup Storage
### Copy to S3
```bash
# Install AWS CLI
pip install awscli
# Configure
aws configure
# Upload backup
LATEST=$(ls -t backups/*.gz | head -1)
aws s3 cp "$LATEST" s3://your-bucket/backups/
```
### Automate S3 Upload
Add to `scripts/backup.sh`:
```bash
# After backup creation
if command -v aws &> /dev/null; then
echo "Uploading to S3..."
aws s3 cp "$BACKUP_FILE" "s3://${S3_BUCKET}/backups/"
fi
```
### Using Laravel Backup Package
For more features, use [spatie/laravel-backup](https://github.com/spatie/laravel-backup):
```bash
composer require spatie/laravel-backup
php artisan vendor:publish --provider="Spatie\Backup\BackupServiceProvider"
```
```php
// config/backup.php
'destination' => [
'disks' => ['local', 's3'],
],
// Schedule
Schedule::command('backup:run')->daily();
Schedule::command('backup:clean')->daily();
```
## Database-Specific Notes
### MySQL
```bash
# Manual backup
docker-compose exec mysql mysqldump -u laravel -p laravel > backup.sql
# Manual restore
docker-compose exec -T mysql mysql -u laravel -p laravel < backup.sql
```
### PostgreSQL
```bash
# Manual backup
docker-compose exec pgsql pg_dump -U laravel laravel > backup.sql
# Manual restore
docker-compose exec -T pgsql psql -U laravel laravel < backup.sql
```
### SQLite
```bash
# Manual backup (just copy the file)
cp src/database/database.sqlite backups/database_backup.sqlite
# Manual restore
cp backups/database_backup.sqlite src/database/database.sqlite
```
## Backup Strategy
### Development
- Manual backups before major changes
- Keep last 5 backups
### Staging
- Daily automated backups
- Keep last 7 days
### Production
- Hourly incremental (if supported)
- Daily full backup
- Weekly backup to offsite storage
- Keep 30 days of backups
- Test restores monthly
## Testing Restores
**Important:** Regularly test your backups!
```bash
# Create test database
docker-compose exec mysql mysql -u root -p -e "CREATE DATABASE restore_test;"
# Restore to test database
gunzip -c backups/latest.sql.gz | docker-compose exec -T mysql mysql -u root -p restore_test
# Verify data
docker-compose exec mysql mysql -u root -p restore_test -e "SELECT COUNT(*) FROM users;"
# Cleanup
docker-compose exec mysql mysql -u root -p -e "DROP DATABASE restore_test;"
```
## Troubleshooting
### Backup fails with permission error
```bash
chmod +x scripts/backup.sh scripts/restore.sh
mkdir -p backups
chmod 755 backups
```
### Restore fails - database locked
```bash
# Stop queue workers
make queue-stop
# Run restore
make restore file=backups/backup.sql.gz
# Restart queue workers
make queue-start
```
### Large database backup timeout
```bash
# Increase timeout in docker-compose.yml
environment:
MYSQL_CONNECT_TIMEOUT: 600
```

263
docs/ci-cd.md Normal file
View File

@@ -0,0 +1,263 @@
# CI/CD Pipeline
This template includes a GitHub Actions workflow for continuous integration and deployment.
## Overview
```
┌─────────────┐ ┌─────────────┐ ┌─────────────────┐
│ Push to │────▶│ Run Tests │────▶│ Deploy Staging │
│ develop │ │ + Lint │ │ (automatic) │
└─────────────┘ └─────────────┘ └─────────────────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────────┐
│ Push to │────▶│ Run Tests │────▶│ Deploy Prod │
│ main │ │ + Lint │ │ (with approval)│
└─────────────┘ └─────────────┘ └─────────────────┘
```
## Workflow File
Located at `.github/workflows/ci.yml`
### Jobs
| Job | Trigger | Description |
|-----|---------|-------------|
| **tests** | All pushes/PRs | Run Pest tests + Pint lint |
| **deploy-staging** | Push to `develop` | Auto-deploy to staging |
| **deploy-production** | Push to `main` | Deploy with approval |
## Setup
### 1. Create GitHub Secrets
Go to: Repository → Settings → Secrets and variables → Actions
**For Staging:**
```
STAGING_HOST - Staging server IP/hostname
STAGING_USER - SSH username
STAGING_SSH_KEY - Private SSH key (full content)
```
**For Production:**
```
PRODUCTION_HOST - Production server IP/hostname
PRODUCTION_USER - SSH username
PRODUCTION_SSH_KEY - Private SSH key (full content)
```
### 2. Generate SSH Key
```bash
# Generate a new key pair
ssh-keygen -t ed25519 -C "github-actions" -f github-actions-key
# Add public key to server
cat github-actions-key.pub >> ~/.ssh/authorized_keys
# Copy private key to GitHub secret
cat github-actions-key
```
### 3. Configure Server
On your server, ensure:
```bash
# Create deployment directory
sudo mkdir -p /var/www/staging
sudo mkdir -p /var/www/production
sudo chown -R $USER:www-data /var/www/staging /var/www/production
# Clone repository
cd /var/www/staging
git clone git@github.com:your-repo.git .
# Install dependencies
composer install
npm install && npm run build
# Configure environment
cp .env.example .env
php artisan key:generate
# Edit .env with production values
# Set permissions
chmod -R 775 storage bootstrap/cache
```
### 4. Environment Protection (Optional)
For production deployments with approval:
1. Go to Repository → Settings → Environments
2. Create `production` environment
3. Enable "Required reviewers"
4. Add team members who can approve
## Workflow Customization
### Add More Tests
```yaml
- name: Run security audit
working-directory: ./src
run: composer audit
- name: Run static analysis
working-directory: ./src
run: ./vendor/bin/phpstan analyse
```
### Add Notifications
```yaml
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,commit,author,action
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
if: always()
```
### Add Database Migrations Check
```yaml
- name: Check pending migrations
working-directory: ./src
run: |
PENDING=$(php artisan migrate:status | grep -c "No" || true)
if [ "$PENDING" -gt 0 ]; then
echo "::warning::There are pending migrations"
fi
```
## Manual Deployment
If you prefer manual deployments:
```bash
# On your server
cd /var/www/production
# Enable maintenance mode
php artisan down
# Pull latest code
git pull origin main
# Install dependencies
composer install --no-dev --optimize-autoloader
# Run migrations
php artisan migrate --force
# Clear and cache
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Restart queue workers
php artisan queue:restart
# Disable maintenance mode
php artisan up
```
## Deployment Script
Create `deploy/scripts/deploy.sh` for reusable deployment:
```bash
#!/bin/bash
set -e
echo "🚀 Starting deployment..."
# Enter maintenance mode
php artisan down
# Pull latest changes
git pull origin main
# Install dependencies
composer install --no-dev --optimize-autoloader
# Run migrations
php artisan migrate --force
# Build assets
npm install
npm run build
# Clear caches
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Restart queue
php artisan queue:restart
# Exit maintenance mode
php artisan up
echo "✅ Deployment complete!"
```
## Rollback
If deployment fails:
```bash
# Revert to previous commit
git reset --hard HEAD~1
# Or specific commit
git reset --hard <commit-hash>
# Re-run caching
php artisan config:cache
php artisan route:cache
# Restart services
php artisan queue:restart
php artisan up
```
## Testing Locally
Test the CI workflow locally with [act](https://github.com/nektos/act):
```bash
# Install act
brew install act # macOS
# or
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
# Run tests job
act -j tests
# Run with secrets
act -j tests --secret-file .secrets
```
## Troubleshooting
### SSH Connection Failed
- Verify SSH key is correct (no extra newlines)
- Check server firewall allows port 22
- Ensure key is added to `~/.ssh/authorized_keys`
### Permission Denied
- Check file ownership: `chown -R www-data:www-data /var/www`
- Check directory permissions: `chmod -R 775 storage bootstrap/cache`
### Composer/NPM Fails
- Ensure sufficient memory on server
- Check PHP extensions are installed
- Verify Node.js version matches requirements

177
docs/error-logging.md Normal file
View File

@@ -0,0 +1,177 @@
# Error Logging Setup
This template uses **Flare + Ignition** by Spatie for error logging.
## Overview
| Environment | Tool | Purpose |
|-------------|------|---------|
| Development | **Ignition** | Rich in-browser error pages with AI explanations |
| Development | **Telescope** (optional) | Request/query/job debugging dashboard |
| Production | **Flare** | Remote error tracking, notifications |
## Architecture
```
Development:
Error → Ignition → Beautiful error page with:
- Stack trace with code context
- AI-powered explanations
- Click-to-open in VS Code
- Suggested solutions
Production:
Error → Flare (remote) → Notifications (Slack/Email)
User sees clean 500.blade.php error page
```
## Setup
### 1. Run Post-Install Script
After creating your Laravel project:
```bash
# In Docker
make setup-tools
# Or manually
cd src
bash ../scripts/post-install.sh
```
### 2. Get Flare API Key
1. Sign up at [flareapp.io](https://flareapp.io)
2. Create a new project
3. Copy your API key
### 3. Configure Environment
**Development (.env):**
```env
FLARE_KEY=your_flare_key_here
IGNITION_THEME=auto
IGNITION_EDITOR=vscode
```
**Production (.env):**
```env
APP_DEBUG=false
FLARE_KEY=your_flare_key_here
```
## Ignition Features (Development)
### AI Error Explanations
Ignition can explain errors using AI. Click "AI" button on any error page.
### Click-to-Open in Editor
Errors link directly to the file and line in your editor.
Supported editors (set via `IGNITION_EDITOR`):
- `vscode` - Visual Studio Code
- `phpstorm` - PhpStorm
- `sublime` - Sublime Text
- `atom` - Atom
- `textmate` - TextMate
### Runnable Solutions
Ignition suggests fixes for common issues that you can apply with one click.
### Share Error Context
Click "Share" to create a shareable link for debugging with teammates.
## Telescope (Optional)
Telescope provides a debug dashboard at `/telescope` with:
- **Requests** - All HTTP requests with timing
- **Exceptions** - All caught exceptions
- **Logs** - Log entries
- **Queries** - Database queries with timing
- **Jobs** - Queue job processing
- **Mail** - Sent emails
- **Notifications** - All notifications
- **Cache** - Cache operations
### Installing Telescope
The post-install script offers to install Telescope. To install manually:
```bash
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
```
### Telescope in Production
Telescope is installed as a dev dependency. For production debugging:
1. Install without `--dev`
2. Configure authorization in `app/Providers/TelescopeServiceProvider.php`
3. Access via `/telescope` (requires authentication)
## Custom Error Pages
The post-install script creates custom error pages:
- `resources/views/errors/404.blade.php` - Not Found
- `resources/views/errors/500.blade.php` - Server Error
- `resources/views/errors/503.blade.php` - Maintenance Mode
These are shown to users in production while Flare captures the full error details.
## Flare Dashboard
In your Flare dashboard you can:
- View all errors with full stack traces
- See request data, session, user info
- Group errors by type
- Track error frequency over time
- Set up notifications (Slack, Email, Discord)
- Mark errors as resolved
## Testing Error Logging
### Test in Development
Add a test route in `routes/web.php`:
```php
Route::get('/test-error', function () {
throw new \Exception('Test error for Flare!');
});
```
Visit `/test-error` to see:
- Ignition error page (development)
- Error logged in Flare dashboard
### Test in Production
```bash
php artisan down # Enable maintenance mode
# Visit site - should see 503 page
php artisan up # Disable maintenance mode
```
## Troubleshooting
### Errors not appearing in Flare
1. Check `FLARE_KEY` is set correctly
2. Verify `APP_ENV=production` and `APP_DEBUG=false`
3. Check network connectivity from server
### Ignition not showing AI explanations
1. Requires OpenAI API key in Flare settings
2. Available on paid Flare plans
### Telescope not loading
1. Run `php artisan telescope:install`
2. Run `php artisan migrate`
3. Clear cache: `php artisan config:clear`

222
docs/filament-admin.md Normal file
View File

@@ -0,0 +1,222 @@
# Filament Admin Panel
> **🤖 AI Agents**: For Filament form/table component syntax, see [CLAUDE.md](../CLAUDE.md#-filament-form-field-reference).
This template includes optional [Filament](https://filamentphp.com/) admin panel setup for user management and administration.
## What is Filament?
Filament is a full-stack admin panel framework for Laravel built on Livewire. It provides:
- **Admin Panel** - Beautiful, responsive dashboard
- **Form Builder** - Dynamic forms with validation
- **Table Builder** - Sortable, searchable, filterable tables
- **User Management** - CRUD for users out of the box
- **Widgets** - Dashboard stats and charts
- **Notifications** - Toast notifications
- **Actions** - Bulk actions, row actions
## Installation
Filament is installed via `make setup-laravel` when you select "Yes" for admin panel.
Manual installation:
```bash
composer require filament/filament:"^3.2" -W
php artisan filament:install --panels
php artisan make:filament-user
php artisan make:filament-resource User --generate
```
## Accessing Admin Panel
- **URL**: `http://localhost:8080/admin`
- **Login**: Use credentials created during setup
## User Management
The setup creates a `UserResource` at `app/Filament/Resources/UserResource.php`.
This provides:
- List all users with search/filter
- Create new users
- Edit existing users
- Delete users
### Customizing User Resource
Edit `app/Filament/Resources/UserResource.php`:
```php
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('email')
->email()
->required()
->unique(ignoreRecord: true),
Forms\Components\DateTimePicker::make('email_verified_at'),
Forms\Components\TextInput::make('password')
->password()
->dehydrateStateUsing(fn ($state) => Hash::make($state))
->dehydrated(fn ($state) => filled($state))
->required(fn (string $context): bool => $context === 'create'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('email')
->searchable(),
Tables\Columns\TextColumn::make('email_verified_at')
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
```
## Adding Roles & Permissions
For role-based access, add Spatie Permission:
```bash
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
```
Then install Filament Shield for admin UI:
```bash
composer require bezhansalleh/filament-shield
php artisan shield:install
```
This adds:
- Role management in admin
- Permission management
- Protect resources by role
## Creating Additional Resources
```bash
# Generate resource for a model
php artisan make:filament-resource Post --generate
# Generate with soft deletes
php artisan make:filament-resource Post --generate --soft-deletes
# Generate simple (modal-based, no separate pages)
php artisan make:filament-resource Post --simple
```
## Dashboard Widgets
Create a stats widget:
```bash
php artisan make:filament-widget StatsOverview --stats-overview
```
Edit `app/Filament/Widgets/StatsOverview.php`:
```php
protected function getStats(): array
{
return [
Stat::make('Total Users', User::count()),
Stat::make('Verified Users', User::whereNotNull('email_verified_at')->count()),
Stat::make('New Today', User::whereDate('created_at', today())->count()),
];
}
```
## Restricting Admin Access
### By Email Domain
In `app/Providers/Filament/AdminPanelProvider.php`:
```php
->authGuard('web')
->login()
->registration(false) // Disable public registration
```
### By User Method
Add to `User` model:
```php
public function canAccessPanel(Panel $panel): bool
{
return str_ends_with($this->email, '@yourcompany.com');
}
```
Or with roles:
```php
public function canAccessPanel(Panel $panel): bool
{
return $this->hasRole('admin');
}
```
## Customizing Theme
Publish and customize:
```bash
php artisan vendor:publish --tag=filament-config
php artisan vendor:publish --tag=filament-panels-translations
```
Edit colors in `app/Providers/Filament/AdminPanelProvider.php`:
```php
->colors([
'primary' => Color::Indigo,
'danger' => Color::Rose,
'success' => Color::Emerald,
])
```
## Production Considerations
1. **Disable registration** in admin panel
2. **Use strong passwords** for admin users
3. **Enable 2FA** if using Jetstream
4. **Restrict by IP** in production if possible
5. **Monitor admin actions** via activity logging
## Resources
- [Filament Documentation](https://filamentphp.com/docs)
- [Filament Plugins](https://filamentphp.com/plugins)
- [Filament Shield (Roles)](https://github.com/bezhanSalleh/filament-shield)

263
docs/laravel-setup.md Normal file
View File

@@ -0,0 +1,263 @@
# Laravel Base Setup Guide
This guide covers setting up authentication, API, and base middleware for your Laravel application.
## Quick Start
After installing Laravel and running `make setup-tools`, run:
```bash
make setup-laravel
```
This interactive script will:
1. Set up authentication (Breeze or Jetstream)
2. Configure Sanctum for API authentication
3. Create security middleware
4. Set up storage symlink
## Authentication Options
> **This template focuses on Blade and Livewire** - no JavaScript frameworks (Vue/React/Inertia). This keeps debugging simple and server-side.
### Laravel Breeze + Blade (Recommended)
Best for: Most applications. Simple, fast, easy to debug.
Features:
- Login, registration, password reset
- Email verification
- Profile editing
- Tailwind CSS styling
```bash
composer require laravel/breeze --dev
php artisan breeze:install blade
php artisan migrate
npm install && npm run build
```
### Laravel Breeze + Livewire
Best for: Apps needing reactive UI without JavaScript frameworks.
Same features as Blade, but with dynamic updates via Livewire.
```bash
composer require laravel/breeze --dev
php artisan breeze:install livewire
php artisan migrate
npm install && npm run build
```
### Laravel Breeze API Only
Best for: When you want to build your own Blade views.
Provides API authentication endpoints, you build the frontend.
```bash
composer require laravel/breeze --dev
php artisan breeze:install api
php artisan migrate
```
### Laravel Jetstream + Livewire (Full-featured)
Best for: SaaS applications needing teams, 2FA, API tokens.
Features:
- Profile management with photo upload
- Two-factor authentication
- API token management
- Team management (optional)
- Session management
- Browser session logout
```bash
composer require laravel/jetstream
php artisan jetstream:install livewire --teams
php artisan migrate
npm install && npm run build
```
## API Authentication (Sanctum)
Laravel Sanctum provides:
- SPA authentication (cookie-based)
- API token authentication
- Mobile app authentication
### Creating Tokens
```php
// Create a token
$token = $user->createToken('api-token')->plainTextToken;
// Create with abilities
$token = $user->createToken('api-token', ['posts:read', 'posts:write'])->plainTextToken;
```
### Authenticating Requests
```bash
# Using token
curl -H "Authorization: Bearer YOUR_TOKEN" https://your-app.com/api/user
# Using cookie (SPA)
# First get CSRF token from /sanctum/csrf-cookie
```
### Token Abilities
```php
// Check ability
if ($user->tokenCan('posts:write')) {
// Can write posts
}
// In route middleware
Route::post('/posts', [PostController::class, 'store'])
->middleware('ability:posts:write');
```
## Security Middleware
The setup script creates two middleware files:
### ForceHttps
Redirects HTTP to HTTPS in production.
```php
// Register in bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\ForceHttps::class);
})
```
### SecurityHeaders
Adds security headers to all responses:
- X-Frame-Options
- X-Content-Type-Options
- X-XSS-Protection
- Referrer-Policy
- Permissions-Policy
- Strict-Transport-Security (production only)
```php
// Register in bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\SecurityHeaders::class);
})
```
## API Routes Template
An example API routes file is provided at `src/routes/api.example.php`.
Key patterns:
- Health check endpoint
- Protected routes with `auth:sanctum`
- Token management endpoints
- API versioning structure
## CORS Configuration
If your API is consumed by a separate frontend:
1. Copy `src/config/cors.php.example` to `config/cors.php`
2. Update `allowed_origins` with your frontend URL
3. Set `FRONTEND_URL` in `.env`
```env
FRONTEND_URL=https://your-frontend.com
```
## Development Workflow
### After Setup
```bash
# Start development
make up DB=mysql
# Run migrations
make artisan cmd='migrate'
# Create a user (tinker)
make tinker
# User::factory()->create(['email' => 'test@example.com'])
# Run tests
make test
# Fix code style
make lint
```
### Common Tasks
```bash
# Create controller
make artisan cmd='make:controller Api/PostController --api'
# Create model with migration
make artisan cmd='make:model Post -m'
# Create form request
make artisan cmd='make:request StorePostRequest'
# Create resource
make artisan cmd='make:resource PostResource'
# Create policy
make artisan cmd='make:policy PostPolicy --model=Post'
```
## Testing API
### With curl
```bash
# Register (if using Breeze API)
curl -X POST http://localhost:8080/api/register \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@test.com","password":"password","password_confirmation":"password"}'
# Login
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"email":"test@test.com","password":"password"}'
# Use token
curl http://localhost:8080/api/user \
-H "Authorization: Bearer YOUR_TOKEN"
```
### With Postman/Insomnia
1. Import the API collection (create from routes)
2. Set base URL to `http://localhost:8080/api`
3. Add Authorization header with Bearer token
## Troubleshooting
### CORS Errors
1. Check `config/cors.php` includes your frontend origin
2. Ensure `supports_credentials` is `true` if using cookies
3. Clear config cache: `php artisan config:clear`
### 401 Unauthorized
1. Check token is valid and not expired
2. Ensure `auth:sanctum` middleware is applied
3. For SPA: ensure CSRF cookie is set
### Session Issues
1. Check `SESSION_DOMAIN` matches your domain
2. For subdomains, use `.yourdomain.com`
3. Ensure Redis is running for session storage

130
docs/menu-management.md Normal file
View File

@@ -0,0 +1,130 @@
# Frontend Menu Management
This template includes a dynamic menu management system that allows administrators to create and manage frontend navigation menus through the admin panel.
## Features
- **Dynamic Menus**: Create multiple menus (header, footer, sidebar, etc.)
- **Nested Items**: Support for parent-child menu items (dropdowns)
- **Multiple Link Types**:
- External links (URLs)
- Named routes
- Module links (auto-generates route from module slug)
- **Permission-Based Filtering**: Menu items are automatically filtered based on user permissions
- **Drag & Drop Reordering**: Reorder menu items in the admin panel
## How It Works
### Permission Filtering
Menu items are automatically filtered based on the current user's permissions:
1. **Module Items**: If a menu item links to a module (e.g., `companies`), the user must have `{module}.view` permission (e.g., `companies.view`) to see that item.
2. **Permission Items**: If a menu item has a specific `permission` set, the user must have that exact permission.
3. **Admin Users**: Users with the `admin` role can see all menu items.
4. **Guest Users**: Only see items with no permission or module restrictions.
## Usage
### In Blade Templates
Use the `<x-frontend-menu>` component to render a menu:
```blade
{{-- Desktop navigation --}}
<x-frontend-menu menu="header" />
{{-- Mobile/responsive navigation --}}
<x-frontend-menu-responsive menu="header" />
{{-- By location --}}
<x-frontend-menu menu="footer" />
```
### Programmatic Access
```php
use App\Services\MenuService;
$menuService = app(MenuService::class);
// Get menu items (already filtered for current user)
$items = $menuService->getMenu('header');
// Get available modules for menu items
$modules = $menuService->getAvailableModules();
// Get available routes for menu items
$routes = $menuService->getAvailableRoutes();
```
## Admin Panel
Navigate to **Settings > Menus** in the admin panel to:
1. Create new menus
2. Add/edit/delete menu items
3. Set item types (link, route, module)
4. Configure permissions for each item
5. Reorder items via drag & drop
## Database Structure
### `menus` table
- `id` - Primary key
- `name` - Display name
- `slug` - Unique identifier (used in templates)
- `location` - Optional location hint (header, footer, sidebar)
- `is_active` - Toggle menu visibility
### `menu_items` table
- `id` - Primary key
- `menu_id` - Foreign key to menus
- `parent_id` - For nested items
- `title` - Display text
- `type` - link, route, or module
- `url` - For external links
- `route` - For named routes
- `route_params` - JSON parameters for routes
- `module` - Module slug for permission checking
- `permission` - Specific permission required
- `icon` - Heroicon name
- `target` - _self or _blank
- `order` - Sort order
- `is_active` - Toggle visibility
## Adding Menu Items for New Modules
When creating a new module, add a menu item to link to it:
1. Go to **Settings > Menus** in admin
2. Edit the "Header Navigation" menu
3. Click "New" in the Items section
4. Set:
- **Title**: Your module name
- **Type**: Module
- **Module**: Select your module from dropdown
5. Save
The menu item will automatically:
- Generate the correct URL (`/{module-slug}`)
- Check for `{module}.view` permission
- Hide from users without permission
## Customization
### Custom Menu Component
Copy and modify the default component:
```bash
cp resources/views/components/frontend-menu.blade.php \
resources/views/components/my-custom-menu.blade.php
```
### Styling
The default components use Tailwind CSS classes. Modify the component templates to match your design.

173
docs/module-generator.md Normal file
View File

@@ -0,0 +1,173 @@
# Module Generator (Admin UI)
A visual tool for creating module skeletons through the Filament admin panel.
> **Development Only**: This tool is only available when `APP_ENV=local`.
## Access
Navigate to: **Admin Panel → Development → Module Generator**
URL: `/admin/module-generator`
## Features
- **Visual Form**: Create modules without command line
- **Auto Git Branch**: Optionally creates `module/{name}` branch and commits files
- **Skeleton Only**: Maximum flexibility - creates structure, you add the models
- **Instant Feedback**: Shows generation logs and next steps
## What Gets Created
```
app/Modules/{ModuleName}/
├── Config/{module_name}.php # Module configuration
├── Database/
│ ├── Migrations/ # Empty, add your migrations
│ └── Seeders/ # Empty, add your seeders
├── Filament/Resources/ # Empty, add Filament resources
├── Http/
│ ├── Controllers/{ModuleName}Controller.php
│ ├── Middleware/ # Empty
│ └── Requests/ # Empty
├── Models/ # Empty, add your models
├── Policies/ # Empty, add policies
├── Services/ # Empty, add services
├── Routes/
│ ├── web.php # Basic index route
│ └── api.php # If API option selected
├── Resources/views/
│ └── index.blade.php # Starter view
├── Permissions.php # CRUD permissions
├── {ModuleName}ServiceProvider.php # Auto-registered
└── README.md # Module documentation
```
## Form Options
| Field | Description |
|-------|-------------|
| **Module Name** | PascalCase name (e.g., `Accounting`, `Inventory`) |
| **Description** | Brief description for README and config |
| **Create Git Branch** | Auto-create `module/{name}` branch and commit |
| **Include API Routes** | Add `Routes/api.php` with Sanctum auth |
## Git Integration
When "Create Git Branch" is enabled:
1. **Checks** for uncommitted changes (fails if dirty)
2. **Creates** branch `module/{module-name}`
3. **Generates** all module files
4. **Commits** with message `feat: Add {ModuleName} module skeleton`
5. **Shows** push command: `git push -u origin module/{name}`
### Requirements
- Working directory must be clean (no uncommitted changes)
- Git must be installed in the container
- Repository must be initialized
## After Generation
1. **Run migrations** (if you add any):
```bash
php artisan migrate
```
2. **Seed permissions**:
```bash
php artisan db:seed --class=RolePermissionSeeder
```
3. **Clear caches**:
```bash
php artisan optimize:clear
```
4. **Push branch** (if Git was used):
```bash
git push -u origin module/{name}
```
## Adding Models
After generating the skeleton, add models manually:
```php
// app/Modules/Accounting/Models/Invoice.php
<?php
namespace App\Modules\Accounting\Models;
use App\Traits\ModuleAuditable;
use Illuminate\Database\Eloquent\Model;
use OwenIt\Auditing\Contracts\Auditable;
class Invoice extends Model implements Auditable
{
use ModuleAuditable;
protected $fillable = [
'number',
'customer_id',
'total',
'status',
];
}
```
## Adding Filament Resources
Create admin CRUD in `Filament/Resources/`:
```php
// app/Modules/Accounting/Filament/Resources/InvoiceResource.php
<?php
namespace App\Modules\Accounting\Filament\Resources;
use App\Modules\Accounting\Models\Invoice;
use Filament\Resources\Resource;
// ... standard Filament resource implementation
```
## Comparison: UI vs CLI
| Feature | UI Generator | `make:module` CLI |
|---------|--------------|-------------------|
| Skeleton only | ✅ | ❌ (includes model) |
| Git integration | ✅ Auto-branch | ❌ Manual |
| Model generation | ❌ | ✅ With `--model=` |
| Filament resource | ❌ | ✅ With `--model=` |
| Visual feedback | ✅ | Terminal output |
| Environment | Local only | Any |
**Use UI Generator when**: You need a blank canvas for complex modules with multiple models.
**Use CLI when**: You want a quick single-model module with all files generated.
## Troubleshooting
### "Git working directory has uncommitted changes"
Commit or stash your changes first:
```bash
git add . && git commit -m "WIP"
# or
git stash
```
### Module not showing in admin
Clear caches:
```bash
php artisan optimize:clear
```
### Permissions not working
Re-seed permissions:
```bash
php artisan db:seed --class=RolePermissionSeeder
```

316
docs/modules.md Normal file
View File

@@ -0,0 +1,316 @@
# Modular Architecture
> **🤖 AI Agents**: For EXACT file templates and copy-paste code, see [CLAUDE.md](../CLAUDE.md).
> This document explains concepts; CLAUDE.md provides strict implementation patterns.
This template uses a modular architecture to organize features into self-contained modules. Each module has its own admin panel, routes, views, and permissions.
## Quick Start
```bash
# Create a new module
php artisan make:module StockManagement
# Create with a model
php artisan make:module StockManagement --model=Product
# Create with API routes
php artisan make:module StockManagement --api
# Skip Filament admin
php artisan make:module StockManagement --no-filament
```
## Module Structure
```
app/Modules/StockManagement/
├── Config/
│ └── stock_management.php # Module configuration
├── Database/
│ ├── Migrations/ # Module-specific migrations
│ └── Seeders/
│ └── StockManagementPermissionSeeder.php
├── Filament/
│ └── Resources/ # Admin panel resources
│ ├── StockManagementDashboardResource.php
│ └── ProductResource.php # If --model used
├── Http/
│ ├── Controllers/
│ │ └── StockManagementController.php
│ ├── Middleware/
│ └── Requests/
├── Models/
│ └── Product.php # If --model used
├── Policies/
├── Services/ # Business logic
├── Routes/
│ ├── web.php # Frontend routes
│ └── api.php # API routes (if --api)
├── Resources/
│ ├── views/
│ │ ├── index.blade.php # Module landing page
│ │ ├── layouts/
│ │ └── filament/
│ ├── css/
│ │ └── stock-management.css # Module-specific styles
│ └── lang/en/
├── Permissions.php # Module permissions
└── StockManagementServiceProvider.php
```
## How Modules Work
### Auto-Loading
The `ModuleServiceProvider` automatically:
- Discovers all modules in `app/Modules/`
- Registers each module's service provider
- Loads routes, views, migrations, and translations
### Routes
Module routes are prefixed and named automatically:
```php
// Routes/web.php
Route::prefix('stock-management')
->name('stock-management.')
->middleware(['web', 'auth'])
->group(function () {
Route::get('/', [StockManagementController::class, 'index'])
->name('index');
});
```
Access: `http://localhost:8080/stock-management`
### Views
Module views use a namespace based on the module slug:
```php
// In controller
return view('stock-management::index');
// In Blade
@include('stock-management::partials.header')
```
### Filament Admin
Each module gets its own navigation group in the admin panel:
```
📦 Stock Management
├── Dashboard
├── Products
└── Inventory
```
Resources are automatically discovered from `Filament/Resources/`.
## Permissions
### Defining Permissions
Each module has a `Permissions.php` file:
```php
// app/Modules/StockManagement/Permissions.php
return [
'stock_management.view' => 'View Stock Management',
'stock_management.create' => 'Create stock records',
'stock_management.edit' => 'Edit stock records',
'stock_management.delete' => 'Delete stock records',
'stock_management.export' => 'Export stock data',
];
```
### Seeding Permissions
After creating a module, run:
```bash
php artisan db:seed --class=PermissionSeeder
```
This registers all module permissions and assigns them to the admin role.
### Using Permissions
In Blade:
```blade
@can('stock_management.view')
<a href="{{ route('stock-management.index') }}">Stock Management</a>
@endcan
```
In Controllers:
```php
public function index()
{
$this->authorize('stock_management.view');
// ...
}
```
In Filament Resources:
```php
public static function canAccess(): bool
{
return auth()->user()?->can('stock_management.view') ?? false;
}
```
## Module Assets
### App-Wide CSS/JS
Use the main `resources/css/app.css` and `resources/js/app.js` for shared styles.
### Module-Specific Styles
Each module has its own CSS file at `Resources/css/{module-slug}.css`.
Include in Blade:
```blade
@push('module-styles')
<link href="{{ asset('modules/stock-management/css/stock-management.css') }}" rel="stylesheet">
@endpush
```
Or inline in the view:
```blade
@push('module-styles')
<style>
.stock-table { /* ... */ }
</style>
@endpush
```
## Creating Models
### With Module Command
```bash
php artisan make:module StockManagement --model=Product
```
Creates:
- `Models/Product.php`
- Migration in `Database/Migrations/`
- Filament resource with CRUD pages
### Adding Models Later
```bash
# From project root
php artisan make:model Modules/StockManagement/Models/Inventory -m
```
Then create the Filament resource:
```bash
php artisan make:filament-resource Inventory \
--model=App\\Modules\\StockManagement\\Models\\Inventory
```
Move the resource to your module's `Filament/Resources/` directory.
## Example: Stock Management Module
### 1. Create the Module
```bash
php artisan make:module StockManagement --model=Product --api
```
### 2. Edit the Migration
```php
// Database/Migrations/xxxx_create_products_table.php
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('sku')->unique();
$table->text('description')->nullable();
$table->decimal('price', 10, 2);
$table->integer('quantity')->default(0);
$table->timestamps();
});
```
### 3. Update the Model
```php
// Models/Product.php
protected $fillable = [
'name',
'sku',
'description',
'price',
'quantity',
];
protected $casts = [
'price' => 'decimal:2',
];
```
### 4. Update Filament Resource
```php
// Filament/Resources/ProductResource.php
public static function form(Form $form): Form
{
return $form->schema([
Forms\Components\TextInput::make('name')->required(),
Forms\Components\TextInput::make('sku')->required()->unique(ignoreRecord: true),
Forms\Components\Textarea::make('description'),
Forms\Components\TextInput::make('price')->numeric()->prefix('$'),
Forms\Components\TextInput::make('quantity')->numeric()->default(0),
]);
}
```
### 5. Run Migrations & Seed
```bash
php artisan migrate
php artisan db:seed --class=PermissionSeeder
```
### 6. Access
- Frontend: `http://localhost:8080/stock-management`
- Admin: `http://localhost:8080/admin` → Stock Management section
## Best Practices
1. **Keep modules independent** - Avoid tight coupling between modules
2. **Use services** - Put business logic in `Services/` not controllers
3. **Define clear permissions** - One permission per action
4. **Use policies** - For complex authorization rules
5. **Module-specific migrations** - Keep data schema with the module
6. **Test modules** - Create tests in `tests/Modules/ModuleName/`
## Troubleshooting
### Module not loading
1. Check service provider exists and is named correctly
2. Clear cache: `php artisan config:clear && php artisan cache:clear`
3. Check `ModuleServiceProvider` is in `bootstrap/providers.php`
### Views not found
1. Verify view namespace matches module slug (kebab-case)
2. Check views are in `Resources/views/`
### Permissions not working
1. Run `php artisan db:seed --class=PermissionSeeder`
2. Clear permission cache: `php artisan permission:cache-reset`
3. Verify user has role with permissions
### Filament resources not showing
1. Check resource is in `Filament/Resources/`
2. Verify `canAccess()` returns true for your user
3. Clear Filament cache: `php artisan filament:cache-components`

275
docs/queues.md Normal file
View File

@@ -0,0 +1,275 @@
# Queues & Background Jobs
This template includes Redis-powered queues for background processing.
## Quick Start
```bash
# Start queue worker
make queue-start
# View queue logs
make queue-logs
# Stop queue worker
make queue-stop
# Restart after code changes
make queue-restart
```
## Configuration
Queue is configured to use Redis in `.env`:
```env
QUEUE_CONNECTION=redis
REDIS_HOST=redis
REDIS_PORT=6379
```
## Creating Jobs
```bash
php artisan make:job ProcessOrder
```
```php
<?php
namespace App\Jobs;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessOrder implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Order $order
) {}
public function handle(): void
{
// Process the order
$this->order->update(['status' => 'processing']);
// Send notification, generate invoice, etc.
}
public function failed(\Throwable $exception): void
{
// Handle failure - log, notify admin, etc.
logger()->error('Order processing failed', [
'order_id' => $this->order->id,
'error' => $exception->getMessage(),
]);
}
}
```
## Dispatching Jobs
```php
// Dispatch immediately to queue
ProcessOrder::dispatch($order);
// Dispatch with delay
ProcessOrder::dispatch($order)->delay(now()->addMinutes(5));
// Dispatch to specific queue
ProcessOrder::dispatch($order)->onQueue('orders');
// Dispatch after response sent
ProcessOrder::dispatchAfterResponse($order);
// Chain jobs
Bus::chain([
new ProcessOrder($order),
new SendOrderConfirmation($order),
new NotifyWarehouse($order),
])->dispatch();
```
## Job Queues
Use different queues for different priorities:
```php
// High priority
ProcessPayment::dispatch($payment)->onQueue('high');
// Default
SendEmail::dispatch($email)->onQueue('default');
// Low priority
GenerateReport::dispatch($report)->onQueue('low');
```
Run workers for specific queues:
```bash
# Process high priority first
php artisan queue:work --queue=high,default,low
```
## Scheduled Jobs
Add to `app/Console/Kernel.php` or `routes/console.php`:
```php
// routes/console.php (Laravel 11+)
use Illuminate\Support\Facades\Schedule;
Schedule::job(new CleanupOldRecords)->daily();
Schedule::job(new SendDailyReport)->dailyAt('08:00');
Schedule::job(new ProcessPendingOrders)->everyFiveMinutes();
// With queue
Schedule::job(new GenerateBackup)->daily()->onQueue('backups');
```
Start scheduler:
```bash
make scheduler-start
```
## Module Jobs
When creating module jobs, place them in the module's directory:
```
app/Modules/Inventory/
├── Jobs/
│ ├── SyncStock.php
│ └── GenerateInventoryReport.php
```
```php
<?php
namespace App\Modules\Inventory\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SyncStock implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $backoff = [60, 300, 900]; // Retry after 1min, 5min, 15min
public function handle(): void
{
// Sync stock levels
}
}
```
## Job Middleware
Rate limit jobs:
```php
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\Middleware\WithoutOverlapping;
class ProcessOrder implements ShouldQueue
{
public function middleware(): array
{
return [
new RateLimited('orders'),
new WithoutOverlapping($this->order->id),
];
}
}
```
## Monitoring
### View Failed Jobs
```bash
php artisan queue:failed
```
### Retry Failed Jobs
```bash
# Retry specific job
php artisan queue:retry <job-id>
# Retry all failed jobs
php artisan queue:retry all
```
### Clear Failed Jobs
```bash
php artisan queue:flush
```
## Production Setup
For production, use Supervisor instead of Docker:
```ini
# /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/html/storage/logs/worker.log
stopwaitsecs=3600
```
```bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
```
## Testing Jobs
```php
use Illuminate\Support\Facades\Queue;
it('dispatches order processing job', function () {
Queue::fake();
$order = Order::factory()->create();
// Trigger action that dispatches job
$order->markAsPaid();
Queue::assertPushed(ProcessOrder::class, function ($job) use ($order) {
return $job->order->id === $order->id;
});
});
it('processes order correctly', function () {
$order = Order::factory()->create(['status' => 'pending']);
// Run job synchronously
(new ProcessOrder($order))->handle();
expect($order->fresh()->status)->toBe('processing');
});
```

286
docs/site-settings.md Normal file
View File

@@ -0,0 +1,286 @@
# Site Settings
Manage your site's appearance (logo, favicon, colors) from the admin panel.
## Overview
Site settings are stored in the database using [spatie/laravel-settings](https://github.com/spatie/laravel-settings) and managed via Filament.
**Admin Location:** `/admin` → Settings → Appearance
## Available Settings
| Setting | Type | Description |
|---------|------|-------------|
| `site_name` | String | Site name (used in title, emails) |
| `logo` | Image | Header logo |
| `favicon` | Image | Browser favicon (32x32) |
| `primary_color` | Color | Primary brand color |
| `secondary_color` | Color | Secondary/accent color |
| `dark_mode` | Boolean | Enable dark mode toggle |
| `footer_text` | Text | Footer copyright text |
## Usage in Blade Templates
### Helper Functions
```blade
{{-- Site name --}}
<h1>{{ site_name() }}</h1>
{{-- Logo with fallback --}}
@if(site_logo())
<img src="{{ site_logo() }}" alt="{{ site_name() }}">
@else
<span>{{ site_name() }}</span>
@endif
{{-- Favicon --}}
<link rel="icon" href="{{ site_favicon() }}">
{{-- Colors --}}
<div style="background: {{ primary_color() }}">Primary</div>
<div style="background: {{ secondary_color() }}">Secondary</div>
{{-- Get any setting --}}
{{ site_settings('footer_text') }}
{{ site_settings('dark_mode') ? 'Dark' : 'Light' }}
```
### Site Head Component
Include in your layout's `<head>`:
```blade
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{-- This adds title, favicon, and CSS variables --}}
<x-site-head :title="$title ?? null" />
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
```
The component automatically:
- Sets the page title with site name
- Adds favicon link
- Creates CSS custom properties for colors
### CSS Custom Properties
After including `<x-site-head />`, these CSS variables are available:
```css
:root {
--primary-color: #3b82f6;
--secondary-color: #64748b;
}
```
Use in your CSS:
```css
.button {
background-color: var(--primary-color);
}
.text-accent {
color: var(--secondary-color);
}
```
Or with Tailwind arbitrary values:
```html
<button class="bg-[var(--primary-color)] text-white">
Click Me
</button>
```
### Utility Classes
The component also creates utility classes:
```html
<div class="bg-primary text-white">Primary background</div>
<div class="text-primary">Primary text</div>
<div class="border-primary">Primary border</div>
<div class="bg-secondary">Secondary background</div>
<button class="btn-primary">Styled Button</button>
```
## Usage in PHP
```php
use App\Settings\SiteSettings;
// Via dependency injection
public function __construct(private SiteSettings $settings)
{
$name = $this->settings->site_name;
}
// Via helper
$name = site_settings('site_name');
$logo = site_logo();
// Via app container
$settings = app(SiteSettings::class);
$color = $settings->primary_color;
```
## Customizing the Settings Page
Edit `app/Filament/Pages/ManageSiteSettings.php`:
### Add New Settings
1. Add property to `app/Settings/SiteSettings.php`:
```php
class SiteSettings extends Settings
{
// ... existing properties
public ?string $contact_email;
public ?string $phone_number;
}
```
2. Create migration in `database/settings/`:
```php
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('site.contact_email', null);
$this->migrator->add('site.phone_number', null);
}
};
```
3. Add to form in `ManageSiteSettings.php`:
```php
Forms\Components\Section::make('Contact')
->schema([
Forms\Components\TextInput::make('contact_email')
->email(),
Forms\Components\TextInput::make('phone_number')
->tel(),
]),
```
4. Run migration: `php artisan migrate`
### Add Social Links
```php
// In SiteSettings.php
public ?array $social_links;
// In migration
$this->migrator->add('site.social_links', []);
// In form
Forms\Components\Repeater::make('social_links')
->schema([
Forms\Components\Select::make('platform')
->options([
'facebook' => 'Facebook',
'twitter' => 'Twitter/X',
'instagram' => 'Instagram',
'linkedin' => 'LinkedIn',
]),
Forms\Components\TextInput::make('url')
->url(),
])
->columns(2),
```
## Caching
Settings are cached automatically. Clear cache after direct database changes:
```bash
php artisan cache:clear
```
Or in code:
```php
app(SiteSettings::class)->refresh();
```
## File Storage
Logo and favicon are stored in `storage/app/public/site/`.
Make sure the storage link exists:
```bash
php artisan storage:link
```
## Example: Complete Layout
```blade
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<x-site-head :title="$title ?? null" />
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100">
{{-- Header with logo --}}
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 py-4">
@if(site_logo())
<img src="{{ site_logo() }}" alt="{{ site_name() }}" class="h-10">
@else
<span class="text-xl font-bold text-primary">{{ site_name() }}</span>
@endif
</div>
</header>
{{-- Content --}}
<main>
{{ $slot }}
</main>
{{-- Footer --}}
<footer class="bg-gray-800 text-white py-8">
<div class="max-w-7xl mx-auto px-4 text-center">
{!! site_settings('footer_text') ?? '© ' . date('Y') . ' ' . site_name() !!}
</div>
</footer>
</div>
</body>
</html>
```
## Troubleshooting
### Settings not updating
1. Clear cache: `php artisan cache:clear`
2. Check file permissions on storage directory
### Logo not showing
1. Run `php artisan storage:link`
2. Check logo exists in `storage/app/public/site/`
### Colors not applying
1. Make sure `<x-site-head />` is in your layout's `<head>`
2. Check browser dev tools for CSS variable values

386
docs/testing.md Normal file
View File

@@ -0,0 +1,386 @@
# Testing with Pest
This template uses [Pest](https://pestphp.com/) for testing - a modern PHP testing framework with elegant syntax.
## Quick Start
```bash
# Run all tests
make test
# Run with coverage
make test-coverage
# Run specific module tests
make test-module module=Inventory
# Run filtered tests
make test-filter filter="can create"
# Run in parallel (faster)
make test-parallel
```
## Test Structure
```
tests/
├── Feature/ # HTTP/integration tests
│ └── ExampleTest.php
├── Unit/ # Isolated unit tests
│ └── ExampleTest.php
├── Modules/ # Module-specific tests
│ └── Inventory/
│ ├── InventoryTest.php
│ └── ProductTest.php
├── Pest.php # Global configuration
├── TestCase.php # Base test class
└── TestHelpers.php # Custom helpers
```
## Writing Tests
### Basic Syntax
```php
// Simple test
it('has a welcome page', function () {
$this->get('/')->assertStatus(200);
});
// With description
test('users can login', function () {
$user = User::factory()->create();
$this->post('/login', [
'email' => $user->email,
'password' => 'password',
])->assertRedirect('/dashboard');
});
```
### Expectations
```php
it('can create a product', function () {
$product = Product::create(['name' => 'Widget', 'price' => 99.99]);
expect($product)
->toBeInstanceOf(Product::class)
->name->toBe('Widget')
->price->toBe(99.99);
});
```
### Grouped Tests
```php
describe('Product Model', function () {
it('can be created', function () {
// ...
});
it('has a price', function () {
// ...
});
it('belongs to a category', function () {
// ...
});
});
```
### Setup/Teardown
```php
beforeEach(function () {
$this->user = User::factory()->create();
});
afterEach(function () {
// Cleanup if needed
});
it('has a user', function () {
expect($this->user)->toBeInstanceOf(User::class);
});
```
## Global Helpers
Defined in `tests/Pest.php`:
```php
// Create a regular user
$user = createUser();
$user = createUser(['name' => 'John']);
// Create an admin user
$admin = createAdmin();
```
## Module Testing
When you create a module with `php artisan make:module`, tests are auto-generated:
```
tests/Modules/Inventory/
├── InventoryTest.php # Route and permission tests
└── ProductTest.php # Model tests (if --model used)
```
### Example Module Test
```php
<?php
use App\Models\User;
use App\Modules\Inventory\Models\Product;
beforeEach(function () {
$this->user = User::factory()->create();
$this->user->givePermissionTo([
'inventory.view',
'inventory.create',
]);
});
describe('Inventory Module', function () {
it('allows authenticated users to view page', function () {
$this->actingAs($this->user)
->get('/inventory')
->assertStatus(200);
});
it('redirects guests to login', function () {
$this->get('/inventory')
->assertRedirect('/login');
});
});
describe('Product Model', function () {
it('can be created', function () {
$product = Product::create([
'name' => 'Test Product',
'price' => 29.99,
]);
expect($product->name)->toBe('Test Product');
});
it('is audited on create', function () {
$this->actingAs($this->user);
$product = Product::create(['name' => 'Audited']);
$this->assertDatabaseHas('audits', [
'auditable_type' => Product::class,
'auditable_id' => $product->id,
'event' => 'created',
]);
});
});
```
## Testing Helpers
Defined in `tests/TestHelpers.php`:
```php
use Tests\TestHelpers;
uses(TestHelpers::class)->in('Modules');
// In your test
it('allows users with permission', function () {
$user = $this->userWithPermissions(['inventory.view']);
$this->actingAs($user)
->get('/inventory')
->assertStatus(200);
});
it('allows module access', function () {
$user = $this->userWithModuleAccess('inventory');
// User has view, create, edit, delete permissions
});
it('audits changes', function () {
$product = Product::create(['name' => 'Test']);
$this->assertAudited($product, 'created');
});
```
## Testing Livewire Components
```php
use Livewire\Livewire;
use App\Livewire\CreateProduct;
it('can create product via Livewire', function () {
$this->actingAs(createUser());
Livewire::test(CreateProduct::class)
->set('name', 'New Product')
->set('price', 49.99)
->call('save')
->assertHasNoErrors()
->assertRedirect('/products');
$this->assertDatabaseHas('products', [
'name' => 'New Product',
]);
});
it('validates required fields', function () {
Livewire::test(CreateProduct::class)
->set('name', '')
->call('save')
->assertHasErrors(['name' => 'required']);
});
```
## Testing Filament Resources
```php
use App\Modules\Inventory\Models\Product;
use function Pest\Livewire\livewire;
it('can list products in admin', function () {
$user = createAdmin();
$products = Product::factory()->count(5)->create();
$this->actingAs($user)
->get('/admin/inventory/products')
->assertSuccessful()
->assertSeeText($products->first()->name);
});
it('can create product from admin', function () {
$user = createAdmin();
livewire(\App\Modules\Inventory\Filament\Resources\ProductResource\Pages\CreateProduct::class)
->fillForm([
'name' => 'Admin Product',
'price' => 100,
])
->call('create')
->assertHasNoFormErrors();
$this->assertDatabaseHas('products', [
'name' => 'Admin Product',
]);
});
```
## Database Testing
Tests use SQLite in-memory for speed. The `LazilyRefreshDatabase` trait only runs migrations when needed.
### Factories
```php
// Create model
$product = Product::factory()->create();
// Create multiple
$products = Product::factory()->count(5)->create();
// With attributes
$product = Product::factory()->create([
'name' => 'Special Product',
'price' => 999.99,
]);
// With state
$product = Product::factory()->active()->create();
```
### Assertions
```php
// Database has record
$this->assertDatabaseHas('products', [
'name' => 'Widget',
]);
// Database missing record
$this->assertDatabaseMissing('products', [
'name' => 'Deleted Product',
]);
// Count records
$this->assertDatabaseCount('products', 5);
```
## HTTP Testing
```php
// GET request
$this->get('/products')->assertStatus(200);
// POST with data
$this->post('/products', [
'name' => 'New Product',
])->assertRedirect('/products');
// JSON API
$this->getJson('/api/products')
->assertStatus(200)
->assertJsonCount(5, 'data');
// With auth
$this->actingAs($user)
->get('/admin')
->assertStatus(200);
// Assert view
$this->get('/products')
->assertViewIs('products.index')
->assertViewHas('products');
```
## Running Specific Tests
```bash
# Single file
php artisan test tests/Feature/ExampleTest.php
# Single test by name
php artisan test --filter="can create product"
# Module tests only
php artisan test tests/Modules/Inventory
# With verbose output
php artisan test --verbose
# Stop on first failure
php artisan test --stop-on-failure
```
## Coverage
```bash
# Generate coverage report
make test-coverage
# Requires Xdebug or PCOV
# Coverage report saved to coverage/
```
## Tips
1. **Use factories** - Don't manually create models in tests
2. **Test behavior, not implementation** - Focus on what, not how
3. **One assertion per test** - Keep tests focused
4. **Use descriptive names** - `it('shows error when email is invalid')`
5. **Test edge cases** - Empty values, boundaries, errors
6. **Run tests often** - Before commits, after changes

85
scripts/backup.sh Normal file
View File

@@ -0,0 +1,85 @@
#!/bin/bash
# Database Backup Script
# Usage: ./scripts/backup.sh
# Creates timestamped backup in backups/ directory
set -e
BACKUP_DIR="backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Create backup directory
mkdir -p "$BACKUP_DIR"
# Detect database type from .env
if [ -f "src/.env" ]; then
DB_CONNECTION=$(grep "^DB_CONNECTION=" src/.env | cut -d '=' -f2)
DB_DATABASE=$(grep "^DB_DATABASE=" src/.env | cut -d '=' -f2)
DB_USERNAME=$(grep "^DB_USERNAME=" src/.env | cut -d '=' -f2)
DB_PASSWORD=$(grep "^DB_PASSWORD=" src/.env | cut -d '=' -f2)
else
echo "Error: src/.env not found"
exit 1
fi
echo "=========================================="
echo "Database Backup"
echo "=========================================="
echo "Connection: $DB_CONNECTION"
echo "Database: $DB_DATABASE"
echo ""
case "$DB_CONNECTION" in
mysql)
BACKUP_FILE="$BACKUP_DIR/${DB_DATABASE}_${TIMESTAMP}.sql"
echo "Creating MySQL backup..."
docker-compose exec -T mysql mysqldump \
-u"$DB_USERNAME" \
-p"$DB_PASSWORD" \
"$DB_DATABASE" > "$BACKUP_FILE"
;;
pgsql)
BACKUP_FILE="$BACKUP_DIR/${DB_DATABASE}_${TIMESTAMP}.sql"
echo "Creating PostgreSQL backup..."
docker-compose exec -T pgsql pg_dump \
-U "$DB_USERNAME" \
"$DB_DATABASE" > "$BACKUP_FILE"
;;
sqlite)
BACKUP_FILE="$BACKUP_DIR/${DB_DATABASE}_${TIMESTAMP}.sqlite"
echo "Creating SQLite backup..."
cp "src/database/database.sqlite" "$BACKUP_FILE"
;;
*)
echo "Error: Unknown database connection: $DB_CONNECTION"
exit 1
;;
esac
# Compress backup
if [ -f "$BACKUP_FILE" ]; then
gzip "$BACKUP_FILE"
BACKUP_FILE="${BACKUP_FILE}.gz"
# Get file size
SIZE=$(ls -lh "$BACKUP_FILE" | awk '{print $5}')
echo ""
echo "✓ Backup created successfully!"
echo " File: $BACKUP_FILE"
echo " Size: $SIZE"
echo ""
# Cleanup old backups (keep last 10)
echo "Cleaning up old backups (keeping last 10)..."
ls -t "$BACKUP_DIR"/*.gz 2>/dev/null | tail -n +11 | xargs -r rm --
# List recent backups
echo ""
echo "Recent backups:"
ls -lht "$BACKUP_DIR"/*.gz 2>/dev/null | head -5
else
echo "Error: Backup file not created"
exit 1
fi

1006
scripts/laravel-setup.sh Normal file

File diff suppressed because it is too large Load Diff

229
scripts/post-install.sh Normal file
View File

@@ -0,0 +1,229 @@
#!/bin/bash
# Post-install script for Laravel project
# Run this after 'composer create-project laravel/laravel'
# Usage: ./scripts/post-install.sh
set -e
echo "=========================================="
echo "Laravel Post-Install Setup"
echo "=========================================="
# Check if we're in a Laravel project
if [ ! -f "artisan" ]; then
echo "Error: Not in a Laravel project directory"
echo "Run this from your Laravel project root (src/)"
exit 1
fi
# Install Ignition (already included in Laravel, but ensure latest)
echo ""
echo "[1/5] Ensuring Ignition is installed..."
composer require spatie/laravel-ignition --dev
# Install Flare for production error tracking
echo ""
echo "[2/5] Installing Flare for production error tracking..."
composer require spatie/laravel-flare
# Install Telescope for development debugging (optional)
echo ""
read -p "Install Laravel Telescope for development debugging? [y/N]: " INSTALL_TELESCOPE
if [[ "$INSTALL_TELESCOPE" =~ ^[Yy]$ ]]; then
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
echo "Telescope installed. Access at: /telescope"
fi
# Publish Flare config
echo ""
echo "[3/5] Publishing Flare configuration..."
php artisan vendor:publish --tag=flare-config
# Setup Laravel Pint (code style)
echo ""
echo "[4/5] Setting up Laravel Pint..."
if [ -f "../src/pint.json" ]; then
cp ../pint.json ./pint.json 2>/dev/null || true
fi
echo "Pint configured. Run: ./vendor/bin/pint"
# Create custom error pages
echo ""
echo "[5/5] Creating custom error pages..."
mkdir -p resources/views/errors
# 500 Error Page
cat > resources/views/errors/500.blade.php << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Server Error</title>
<style>
body {
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 20px;
}
.container {
background: white;
border-radius: 16px;
padding: 48px;
text-align: center;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
max-width: 500px;
}
h1 { color: #1f2937; font-size: 72px; margin: 0; }
h2 { color: #4b5563; font-weight: 500; margin: 16px 0; }
p { color: #6b7280; line-height: 1.6; }
a {
display: inline-block;
margin-top: 24px;
padding: 12px 24px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 500;
}
a:hover { background: #5a67d8; }
</style>
</head>
<body>
<div class="container">
<h1>500</h1>
<h2>Something went wrong</h2>
<p>We're sorry, but something unexpected happened. Our team has been notified and is working on it.</p>
<a href="/">Go Home</a>
</div>
</body>
</html>
EOF
# 404 Error Page
cat > resources/views/errors/404.blade.php << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page Not Found</title>
<style>
body {
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 20px;
}
.container {
background: white;
border-radius: 16px;
padding: 48px;
text-align: center;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
max-width: 500px;
}
h1 { color: #1f2937; font-size: 72px; margin: 0; }
h2 { color: #4b5563; font-weight: 500; margin: 16px 0; }
p { color: #6b7280; line-height: 1.6; }
a {
display: inline-block;
margin-top: 24px;
padding: 12px 24px;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 500;
}
a:hover { background: #5a67d8; }
</style>
</head>
<body>
<div class="container">
<h1>404</h1>
<h2>Page not found</h2>
<p>The page you're looking for doesn't exist or has been moved.</p>
<a href="/">Go Home</a>
</div>
</body>
</html>
EOF
# 503 Maintenance Page
cat > resources/views/errors/503.blade.php << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Maintenance Mode</title>
<style>
body {
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 20px;
}
.container {
background: white;
border-radius: 16px;
padding: 48px;
text-align: center;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
max-width: 500px;
}
h1 { color: #1f2937; font-size: 48px; margin: 0; }
h2 { color: #4b5563; font-weight: 500; margin: 16px 0; }
p { color: #6b7280; line-height: 1.6; }
.icon { font-size: 64px; margin-bottom: 16px; }
</style>
</head>
<body>
<div class="container">
<div class="icon">🔧</div>
<h1>Be Right Back</h1>
<h2>We're doing some maintenance</h2>
<p>We're making some improvements and will be back shortly. Thanks for your patience!</p>
</div>
</body>
</html>
EOF
echo ""
echo "=========================================="
echo "Post-install setup complete!"
echo "=========================================="
echo ""
echo "Next steps:"
echo " 1. Get your Flare key at: https://flareapp.io"
echo " 2. Add FLARE_KEY to your .env file"
echo " 3. Customize error pages in resources/views/errors/"
echo ""
echo "Ignition features (development):"
echo " - AI error explanations"
echo " - Click-to-open in VS Code"
echo " - Runnable solution suggestions"
echo ""
if [[ "$INSTALL_TELESCOPE" =~ ^[Yy]$ ]]; then
echo "Telescope dashboard: http://localhost:8080/telescope"
echo ""
fi

101
scripts/restore.sh Normal file
View File

@@ -0,0 +1,101 @@
#!/bin/bash
# Database Restore Script
# Usage: ./scripts/restore.sh <backup_file>
# Example: ./scripts/restore.sh backups/laravel_20240306_120000.sql.gz
set -e
if [ -z "$1" ]; then
echo "Usage: ./scripts/restore.sh <backup_file>"
echo ""
echo "Available backups:"
ls -lht backups/*.gz 2>/dev/null || echo " No backups found"
exit 1
fi
BACKUP_FILE="$1"
if [ ! -f "$BACKUP_FILE" ]; then
echo "Error: Backup file not found: $BACKUP_FILE"
exit 1
fi
# Detect database type from .env
if [ -f "src/.env" ]; then
DB_CONNECTION=$(grep "^DB_CONNECTION=" src/.env | cut -d '=' -f2)
DB_DATABASE=$(grep "^DB_DATABASE=" src/.env | cut -d '=' -f2)
DB_USERNAME=$(grep "^DB_USERNAME=" src/.env | cut -d '=' -f2)
DB_PASSWORD=$(grep "^DB_PASSWORD=" src/.env | cut -d '=' -f2)
else
echo "Error: src/.env not found"
exit 1
fi
echo "=========================================="
echo "Database Restore"
echo "=========================================="
echo "Connection: $DB_CONNECTION"
echo "Database: $DB_DATABASE"
echo "File: $BACKUP_FILE"
echo ""
read -p "⚠️ This will OVERWRITE the current database. Continue? [y/N]: " CONFIRM
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
echo "Restore cancelled."
exit 0
fi
# Decompress if needed
if [[ "$BACKUP_FILE" == *.gz ]]; then
echo "Decompressing backup..."
TEMP_FILE=$(mktemp)
gunzip -c "$BACKUP_FILE" > "$TEMP_FILE"
RESTORE_FILE="$TEMP_FILE"
else
RESTORE_FILE="$BACKUP_FILE"
fi
case "$DB_CONNECTION" in
mysql)
echo "Restoring MySQL database..."
docker-compose exec -T mysql mysql \
-u"$DB_USERNAME" \
-p"$DB_PASSWORD" \
"$DB_DATABASE" < "$RESTORE_FILE"
;;
pgsql)
echo "Restoring PostgreSQL database..."
# Drop and recreate database
docker-compose exec -T pgsql psql \
-U "$DB_USERNAME" \
-c "DROP DATABASE IF EXISTS $DB_DATABASE;"
docker-compose exec -T pgsql psql \
-U "$DB_USERNAME" \
-c "CREATE DATABASE $DB_DATABASE;"
docker-compose exec -T pgsql psql \
-U "$DB_USERNAME" \
-d "$DB_DATABASE" < "$RESTORE_FILE"
;;
sqlite)
echo "Restoring SQLite database..."
cp "$RESTORE_FILE" "src/database/database.sqlite"
chmod 666 "src/database/database.sqlite"
;;
*)
echo "Error: Unknown database connection: $DB_CONNECTION"
exit 1
;;
esac
# Cleanup temp file
if [ -n "$TEMP_FILE" ] && [ -f "$TEMP_FILE" ]; then
rm "$TEMP_FILE"
fi
echo ""
echo "✓ Database restored successfully!"
echo ""
echo "You may want to run:"
echo " make artisan cmd='cache:clear'"
echo " make artisan cmd='config:clear'"

255
setup.bat Normal file
View File

@@ -0,0 +1,255 @@
@echo off
REM Laravel Docker Development Template - Quick Setup (Windows)
REM Everything is pre-installed! This just starts Docker and sets up the database.
REM Usage: setup.bat [mysql|pgsql|sqlite]
setlocal enabledelayedexpansion
set DB=%1
if "%DB%"=="" set DB=mysql
REM Validate database choice
if not "%DB%"=="mysql" if not "%DB%"=="pgsql" if not "%DB%"=="sqlite" (
echo Error: Invalid database '%DB%'
echo Usage: setup.bat [mysql^|pgsql^|sqlite]
echo Example: setup.bat mysql
exit /b 1
)
echo ========================================================
echo Laravel Docker Development Template
echo Quick Setup - Everything Pre-Installed!
echo ========================================================
echo.
echo Setting up with %DB%...
echo.
REM Step 1: Check port availability and auto-assign alternatives
echo Checking port availability...
REM Function to find next available port
REM Usage: call :find_port <start_port> <result_var>
goto :skip_functions
:find_port
set /a port=%~1
:find_port_loop
netstat -ano | findstr ":%port% " >nul 2>&1
if %errorlevel% equ 0 (
set /a port+=1
goto :find_port_loop
)
set %~2=%port%
exit /b 0
:skip_functions
REM Check and assign APP_PORT (Web Server)
netstat -ano | findstr ":8080" >nul 2>&1
if %errorlevel% equ 0 (
call :find_port 8081 APP_PORT
echo [!] Port 8080 in use - using !APP_PORT! for Web Server
) else (
set APP_PORT=8080
echo [OK] Port 8080 ^(Web Server^) - Available
)
REM Check and assign MAIL_DASHBOARD_PORT (Mailpit Dashboard)
netstat -ano | findstr ":8025" >nul 2>&1
if %errorlevel% equ 0 (
call :find_port 8026 MAIL_DASHBOARD_PORT
echo [!] Port 8025 in use - using !MAIL_DASHBOARD_PORT! for Mailpit Dashboard
) else (
set MAIL_DASHBOARD_PORT=8025
echo [OK] Port 8025 ^(Mailpit Dashboard^) - Available
)
REM Check and assign MAIL_PORT (Mailpit SMTP)
netstat -ano | findstr ":1025" >nul 2>&1
if %errorlevel% equ 0 (
call :find_port 1026 MAIL_PORT
echo [!] Port 1025 in use - using !MAIL_PORT! for Mailpit SMTP
) else (
set MAIL_PORT=1025
echo [OK] Port 1025 ^(Mailpit SMTP^) - Available
)
REM Check and assign REDIS_PORT
netstat -ano | findstr ":6379" >nul 2>&1
if %errorlevel% equ 0 (
call :find_port 6380 REDIS_PORT
echo [!] Port 6379 in use - using !REDIS_PORT! for Redis
) else (
set REDIS_PORT=6379
echo [OK] Port 6379 ^(Redis^) - Available
)
REM Check and assign DB_PORT based on database selection
if "%DB%"=="mysql" (
netstat -ano | findstr ":3306" >nul 2>&1
if !errorlevel! equ 0 (
call :find_port 3307 DB_PORT
echo [!] Port 3306 in use - using !DB_PORT! for MySQL
) else (
set DB_PORT=3306
echo [OK] Port 3306 ^(MySQL^) - Available
)
)
if "%DB%"=="pgsql" (
netstat -ano | findstr ":5432" >nul 2>&1
if !errorlevel! equ 0 (
call :find_port 5433 DB_PORT
echo [!] Port 5432 in use - using !DB_PORT! for PostgreSQL
) else (
set DB_PORT=5432
echo [OK] Port 5432 ^(PostgreSQL^) - Available
)
)
echo.
REM Step 2: Configure environment
echo Configuring environment...
if exist "src\.env.%DB%" (
copy /y "src\.env.%DB%" "src\.env" >nul
REM Append port configurations to .env (for reference only, not used by Laravel)
REM Note: DB_PORT is NOT appended - Laravel uses internal Docker port (3306/5432)
REM These are for docker-compose external port mapping only
echo. >> src\.env
echo # Port Configuration ^(auto-assigned by setup^) >> src\.env
echo # APP_PORT=%APP_PORT% >> src\.env
echo # MAIL_DASHBOARD_PORT=%MAIL_DASHBOARD_PORT% >> src\.env
echo # MAIL_PORT=%MAIL_PORT% >> src\.env
echo # REDIS_PORT=%REDIS_PORT% >> src\.env
echo Environment configured for %DB%
) else (
echo ERROR: Template file src\.env.%DB% not found
exit /b 1
)
REM Step 3: Build containers
echo Building Docker containers...
docker-compose build
echo Containers built
echo.
REM Step 4: Start containers
echo Starting Docker containers...
docker-compose --profile %DB% up -d
echo Containers started
echo.
REM Step 5: Wait for app container to be ready
echo Waiting for app container...
timeout /t 3 /nobreak >nul
echo App container ready
echo.
REM Step 6: Wait for database
if "%DB%"=="mysql" (
echo Waiting for MySQL to be ready...
set DB_READY=0
for /L %%i in (1,1,30) do (
docker-compose exec -T app php -r "new PDO('mysql:host=mysql;dbname=laravel', 'laravel', 'secret');" >nul 2>&1
if !errorlevel! equ 0 (
set DB_READY=1
goto :mysql_ready
)
timeout /t 1 /nobreak >nul
)
:mysql_ready
if !DB_READY! equ 1 (
echo MySQL ready
) else (
echo WARNING: MySQL may not be fully ready yet
)
echo.
)
if "%DB%"=="pgsql" (
echo Waiting for PostgreSQL to be ready...
set DB_READY=0
for /L %%i in (1,1,30) do (
docker-compose exec -T app php -r "new PDO('pgsql:host=pgsql;dbname=laravel', 'laravel', 'secret');" >nul 2>&1
if !errorlevel! equ 0 (
set DB_READY=1
goto :pgsql_ready
)
timeout /t 1 /nobreak >nul
)
:pgsql_ready
if !DB_READY! equ 1 (
echo PostgreSQL ready
) else (
echo WARNING: PostgreSQL may not be fully ready yet
)
echo.
)
REM Step 7: Generate app key
echo Generating application key...
docker-compose exec -T app php artisan key:generate --force
echo App key generated
echo.
REM Step 8: Run migrations
echo Running database migrations...
docker-compose exec -T app php artisan migrate --force
echo Migrations completed
echo.
REM Step 9: Seed database
echo Seeding database (roles, permissions, admin user)...
docker-compose exec -T app php artisan db:seed --force
echo Database seeded
echo.
REM Step 10: Create storage link
echo Creating storage symlink...
docker-compose exec -T app php artisan storage:link
echo Storage linked
echo.
REM Step 11: Build frontend assets
echo Building frontend assets...
docker-compose run --rm node npm install >nul 2>&1
docker-compose run --rm node npm run build >nul 2>&1
echo Frontend assets built
echo.
REM Done
echo.
echo ========================================================
echo Setup Complete!
echo ========================================================
echo.
echo Your Laravel application is ready!
echo.
echo Laravel App: http://localhost:%APP_PORT%
echo Admin Panel: http://localhost:%APP_PORT%/admin
echo Mailpit: http://localhost:%MAIL_DASHBOARD_PORT%
echo.
echo Admin Login:
echo Email: admin@example.com
echo Password: password
echo.
echo What's Included:
echo - Laravel 11 with Breeze authentication
echo - Filament admin panel with user management
echo - Pest testing framework
echo - Laravel Pint code style
echo - Queue workers ^& scheduler (optional profiles)
echo.
echo Common Commands:
echo docker-compose exec app php artisan [command]
echo docker-compose exec app composer [command]
echo docker-compose exec app ./vendor/bin/pest
echo docker-compose logs -f app
echo.
echo Stop containers:
echo docker-compose down
echo.
endlocal

237
setup.sh Normal file
View File

@@ -0,0 +1,237 @@
#!/bin/bash
# Laravel Docker Development Template - Quick Setup
# Everything is pre-installed! This just starts Docker and sets up the database.
# Usage: ./setup.sh [mysql|pgsql|sqlite]
set -e
DB=${1:-mysql}
VALID_DBS=("mysql" "pgsql" "sqlite")
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
echo -e "${GREEN}╔════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Laravel Docker Development Template ║${NC}"
echo -e "${GREEN}║ Quick Setup - Everything Pre-Installed! ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════╝${NC}"
echo ""
# Validate database choice
if [[ ! " ${VALID_DBS[@]} " =~ " ${DB} " ]]; then
echo -e "${RED}Error: Invalid database '${DB}'${NC}"
echo "Usage: ./setup.sh [mysql|pgsql|sqlite]"
echo "Example: ./setup.sh mysql"
exit 1
fi
echo -e "${YELLOW}Setting up with ${DB}...${NC}"
echo ""
# Step 1: Check port availability and auto-assign alternatives
echo -e "${YELLOW}→ Checking port availability...${NC}"
# Function to find next available port
find_port() {
local port=$1
while lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":$port "; do
port=$((port + 1))
done
echo $port
}
# Check and assign APP_PORT (Web Server)
if lsof -Pi :8080 -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":8080 "; then
APP_PORT=$(find_port 8081)
echo -e "${YELLOW}[!] Port 8080 in use - using $APP_PORT for Web Server${NC}"
else
APP_PORT=8080
echo -e "${GREEN}[✓] Port 8080 (Web Server) - Available${NC}"
fi
# Check and assign MAIL_DASHBOARD_PORT (Mailpit Dashboard)
if lsof -Pi :8025 -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":8025 "; then
MAIL_DASHBOARD_PORT=$(find_port 8026)
echo -e "${YELLOW}[!] Port 8025 in use - using $MAIL_DASHBOARD_PORT for Mailpit Dashboard${NC}"
else
MAIL_DASHBOARD_PORT=8025
echo -e "${GREEN}[✓] Port 8025 (Mailpit Dashboard) - Available${NC}"
fi
# Check and assign MAIL_PORT (Mailpit SMTP)
if lsof -Pi :1025 -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":1025 "; then
MAIL_PORT=$(find_port 1026)
echo -e "${YELLOW}[!] Port 1025 in use - using $MAIL_PORT for Mailpit SMTP${NC}"
else
MAIL_PORT=1025
echo -e "${GREEN}[✓] Port 1025 (Mailpit SMTP) - Available${NC}"
fi
# Check and assign REDIS_PORT
if lsof -Pi :6379 -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":6379 "; then
REDIS_PORT=$(find_port 6380)
echo -e "${YELLOW}[!] Port 6379 in use - using $REDIS_PORT for Redis${NC}"
else
REDIS_PORT=6379
echo -e "${GREEN}[✓] Port 6379 (Redis) - Available${NC}"
fi
# Check and assign DB_PORT based on database selection
if [ "$DB" = "mysql" ]; then
if lsof -Pi :3306 -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":3306 "; then
DB_PORT=$(find_port 3307)
echo -e "${YELLOW}[!] Port 3306 in use - using $DB_PORT for MySQL${NC}"
else
DB_PORT=3306
echo -e "${GREEN}[✓] Port 3306 (MySQL) - Available${NC}"
fi
elif [ "$DB" = "pgsql" ]; then
if lsof -Pi :5432 -sTCP:LISTEN -t >/dev/null 2>&1 || netstat -tuln 2>/dev/null | grep -q ":5432 "; then
DB_PORT=$(find_port 5433)
echo -e "${YELLOW}[!] Port 5432 in use - using $DB_PORT for PostgreSQL${NC}"
else
DB_PORT=5432
echo -e "${GREEN}[✓] Port 5432 (PostgreSQL) - Available${NC}"
fi
fi
echo ""
# Step 2: Configure environment
echo -e "${YELLOW}→ Configuring environment...${NC}"
if [ -f "src/.env.${DB}" ]; then
cp "src/.env.${DB}" "src/.env"
# Create .env in project root for docker compose port mapping
echo "# Docker Compose Port Configuration (auto-assigned by setup)" > .env
echo "APP_PORT=$APP_PORT" >> .env
echo "MAIL_DASHBOARD_PORT=$MAIL_DASHBOARD_PORT" >> .env
echo "MAIL_PORT=$MAIL_PORT" >> .env
echo "REDIS_PORT=$REDIS_PORT" >> .env
if [ -n "$DB_PORT" ]; then
echo "DB_PORT=$DB_PORT" >> .env
fi
echo -e "${GREEN}✓ Environment configured for ${DB}${NC}"
else
echo -e "${RED}✗ Template file src/.env.${DB} not found${NC}"
exit 1
fi
# Step 3: Build containers
echo -e "${YELLOW}→ Building Docker containers...${NC}"
docker compose build
echo -e "${GREEN}✓ Containers built${NC}"
echo ""
# Step 4: Start containers
echo -e "${YELLOW}→ Starting Docker containers...${NC}"
docker compose --profile ${DB} up -d
echo -e "${GREEN}✓ Containers started${NC}"
echo ""
# Step 5: Wait for app container to be ready
echo -e "${YELLOW}→ Waiting for app container...${NC}"
sleep 3
echo -e "${GREEN}✓ App container ready${NC}"
echo ""
# Step 5.5: Fix permissions on storage, bootstrap/cache, public, and .env
echo -e "${YELLOW}→ Setting directory permissions...${NC}"
chmod -R 777 src/storage src/bootstrap/cache src/public 2>/dev/null || true
chmod 666 src/.env 2>/dev/null || true
docker compose exec -T app chmod -R 777 storage bootstrap/cache public 2>/dev/null || true
docker compose exec -T app chmod 666 .env 2>/dev/null || true
echo -e "${GREEN}✓ Permissions set${NC}"
echo ""
# Step 6: Wait for database
if [ "$DB" = "mysql" ]; then
echo -e "${YELLOW}→ Waiting for MySQL to be ready...${NC}"
for i in {1..30}; do
if docker compose exec -T app php -r "new PDO('mysql:host=mysql;dbname=laravel', 'laravel', 'secret');" >/dev/null 2>&1; then
echo -e "${GREEN}✓ MySQL ready${NC}"
break
fi
sleep 1
done
echo ""
elif [ "$DB" = "pgsql" ]; then
echo -e "${YELLOW}→ Waiting for PostgreSQL to be ready...${NC}"
for i in {1..30}; do
if docker compose exec -T app php -r "new PDO('pgsql:host=pgsql;dbname=laravel', 'laravel', 'secret');" >/dev/null 2>&1; then
echo -e "${GREEN}✓ PostgreSQL ready${NC}"
break
fi
sleep 1
done
echo ""
fi
# Step 7: Generate app key
echo -e "${YELLOW}→ Generating application key...${NC}"
docker compose exec app php artisan key:generate --force
echo -e "${GREEN}✓ App key generated${NC}"
echo ""
# Step 8: Run migrations
echo -e "${YELLOW}→ Running database migrations...${NC}"
docker compose exec app php artisan migrate --force
echo -e "${GREEN}✓ Migrations completed${NC}"
echo ""
# Step 9: Seed database (roles, permissions, admin user)
echo -e "${YELLOW}→ Seeding database (roles, permissions, admin user)...${NC}"
docker compose exec app php artisan db:seed --force
echo -e "${GREEN}✓ Database seeded${NC}"
echo ""
# Step 10: Create storage link
echo -e "${YELLOW}→ Creating storage symlink...${NC}"
docker compose exec app php artisan storage:link
echo -e "${GREEN}✓ Storage linked${NC}"
echo ""
# Step 11: Build frontend assets
echo -e "${YELLOW}→ Building frontend assets...${NC}"
docker compose run --rm node npm install >/dev/null 2>&1
docker compose run --rm node npm run build >/dev/null 2>&1
echo -e "${GREEN}✓ Frontend assets built${NC}"
echo ""
# Done!
echo ""
echo -e "${GREEN}╔════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ 🎉 Setup Complete! 🎉 ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${GREEN}Your Laravel application is ready!${NC}"
echo ""
echo -e " 📱 Laravel App: ${GREEN}http://localhost:$APP_PORT${NC}"
echo -e " 🔐 Admin Panel: ${GREEN}http://localhost:$APP_PORT/admin${NC}"
echo -e " 📧 Mailpit: ${GREEN}http://localhost:$MAIL_DASHBOARD_PORT${NC}"
echo ""
echo -e "${YELLOW}Admin Login:${NC}"
echo -e " Email: admin@example.com"
echo -e " Password: password"
echo ""
echo -e "${YELLOW}What's Included:${NC}"
echo -e " ✓ Laravel 11 with Breeze authentication"
echo -e " ✓ Filament admin panel with user management"
echo -e " ✓ Pest testing framework"
echo -e " ✓ Laravel Pint code style"
echo -e " ✓ Queue workers & scheduler (optional profiles)"
echo ""
echo -e "${YELLOW}Common Commands:${NC}"
echo -e " docker compose exec app php artisan <command>"
echo -e " docker compose exec app composer <command>"
echo -e " docker compose exec app ./vendor/bin/pest"
echo -e " docker compose logs -f app"
echo ""
echo -e "${YELLOW}Stop containers:${NC}"
echo -e " docker compose down"
echo ""

18
src/.editorconfig Normal file
View File

@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4

66
src/.env.example Normal file
View File

@@ -0,0 +1,66 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

64
src/.env.mysql Normal file
View File

@@ -0,0 +1,64 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_URL=http://localhost:8080
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
APP_MAINTENANCE_STORE=database
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis
CACHE_STORE=redis
CACHE_PREFIX=laravel_cache
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

64
src/.env.pgsql Normal file
View File

@@ -0,0 +1,64 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_URL=http://localhost:8080
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
APP_MAINTENANCE_STORE=database
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=pgsql
DB_HOST=pgsql
DB_PORT=5432
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis
CACHE_STORE=redis
CACHE_PREFIX=laravel_cache
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

60
src/.env.sqlite Normal file
View File

@@ -0,0 +1,60 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_TIMEZONE=UTC
APP_URL=http://localhost:8080
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
APP_MAINTENANCE_STORE=database
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
DB_DATABASE=/var/www/html/database/database.sqlite
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis
CACHE_STORE=redis
CACHE_PREFIX=laravel_cache
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

11
src/.gitattributes vendored Normal file
View File

@@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

23
src/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
/auth.json
/.fleet
/.idea
/.nova
/.vscode
/.zed

66
src/README.md Normal file
View File

@@ -0,0 +1,66 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[WebReinvent](https://webreinvent.com/)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Jump24](https://jump24.co.uk)**
- **[Redberry](https://redberry.international/laravel/)**
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class MakeAdminCommand extends Command
{
protected $signature = 'make:admin
{email : The email address for the admin user}
{--name= : The name for the admin user}
{--password= : The password (will prompt if not provided)}';
protected $description = 'Create a new admin user with full permissions';
public function handle(): int
{
$email = $this->argument('email');
$name = $this->option('name') ?? $this->ask('Enter admin name', 'Admin');
$password = $this->option('password') ?? $this->secret('Enter password');
// Validate email
$validator = Validator::make(
['email' => $email],
['email' => 'required|email|unique:users,email']
);
if ($validator->fails()) {
$this->error('Validation failed:');
foreach ($validator->errors()->all() as $error) {
$this->error(" - {$error}");
}
return Command::FAILURE;
}
// Validate password
if (strlen($password) < 8) {
$this->error('Password must be at least 8 characters.');
return Command::FAILURE;
}
// Create user
$user = User::create([
'name' => $name,
'email' => $email,
'password' => Hash::make($password),
'email_verified_at' => now(),
]);
// Assign admin role
$user->assignRole('admin');
$this->info("✅ Admin user created successfully!");
$this->table(
['Field', 'Value'],
[
['Name', $name],
['Email', $email],
['Role', 'admin'],
]
);
$this->newLine();
$this->info("Login at: /admin");
return Command::SUCCESS;
}
}

View File

@@ -0,0 +1,597 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class MakeModuleCommand extends Command
{
protected $signature = 'make:module
{name : The name of the module (PascalCase)}
{--model= : Create a model with the given name}
{--api : Include API routes}
{--no-filament : Skip Filament resource generation}';
protected $description = 'Create a new module with full structure (ServiceProvider, Config, Routes, Views, Permissions)';
protected string $studlyName;
protected string $kebabName;
protected string $snakeName;
protected string $modulePath;
protected ?string $modelName;
public function handle(): int
{
$name = $this->argument('name');
$this->studlyName = Str::studly($name);
$this->kebabName = Str::kebab($name);
$this->snakeName = Str::snake($name);
$this->modelName = $this->option('model') ? Str::studly($this->option('model')) : null;
$this->modulePath = app_path("Modules/{$this->studlyName}");
if (File::exists($this->modulePath)) {
$this->error("Module {$this->studlyName} already exists!");
return self::FAILURE;
}
$this->info("Creating module: {$this->studlyName}");
$this->newLine();
$this->createDirectoryStructure();
$this->createServiceProvider();
$this->createConfig();
$this->createPermissions();
$this->createController();
$this->createRoutes();
$this->createViews();
if ($this->modelName) {
$this->createModel();
$this->createMigration();
if (!$this->option('no-filament')) {
$this->createFilamentResource();
}
}
$this->createTests();
$this->registerServiceProvider();
$this->newLine();
$this->info("✅ Module {$this->studlyName} created successfully!");
$this->newLine();
$this->warn("Next steps:");
$this->line(" 1. Run migrations: <info>php artisan migrate</info>");
$this->line(" 2. Seed permissions: <info>php artisan db:seed --class=RolePermissionSeeder</info>");
$this->line(" 3. Clear caches: <info>php artisan optimize:clear</info>");
$this->newLine();
$this->line(" Access at:");
$this->line(" - Frontend: <info>/{$this->kebabName}</info>");
if ($this->modelName && !$this->option('no-filament')) {
$this->line(" - Admin: <info>/admin → {$this->studlyName}</info>");
}
if ($this->option('api')) {
$this->line(" - API: <info>/api/{$this->kebabName}</info>");
}
return self::SUCCESS;
}
protected function createDirectoryStructure(): void
{
$directories = [
'',
'/Config',
'/Database/Migrations',
'/Database/Seeders',
'/Filament/Resources',
'/Http/Controllers',
'/Http/Middleware',
'/Http/Requests',
'/Models',
'/Policies',
'/Services',
'/Routes',
'/Resources/views',
];
foreach ($directories as $dir) {
File::makeDirectory("{$this->modulePath}{$dir}", 0755, true);
}
$this->info("✓ Created directory structure");
}
protected function createServiceProvider(): void
{
$stub = <<<PHP
<?php
namespace App\Modules\\{$this->studlyName};
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Route;
class {$this->studlyName}ServiceProvider extends ServiceProvider
{
public function register(): void
{
\$this->mergeConfigFrom(
__DIR__ . '/Config/{$this->snakeName}.php',
'{$this->snakeName}'
);
}
public function boot(): void
{
\$this->loadMigrationsFrom(__DIR__ . '/Database/Migrations');
\$this->loadViewsFrom(__DIR__ . '/Resources/views', '{$this->kebabName}');
\$this->registerRoutes();
\$this->registerPermissions();
}
protected function registerRoutes(): void
{
Route::middleware(['web', 'auth'])
->prefix('{$this->kebabName}')
->name('{$this->kebabName}.')
->group(__DIR__ . '/Routes/web.php');
if (file_exists(__DIR__ . '/Routes/api.php')) {
Route::middleware(['api', 'auth:sanctum'])
->prefix('api/{$this->kebabName}')
->name('api.{$this->kebabName}.')
->group(__DIR__ . '/Routes/api.php');
}
}
protected function registerPermissions(): void
{
// Permissions are registered via RolePermissionSeeder
// See: Permissions.php in this module
}
}
PHP;
File::put("{$this->modulePath}/{$this->studlyName}ServiceProvider.php", $stub);
$this->info("✓ Created ServiceProvider");
}
protected function createConfig(): void
{
$stub = <<<PHP
<?php
return [
'name' => '{$this->studlyName}',
'slug' => '{$this->kebabName}',
'version' => '1.0.0',
'audit' => [
'enabled' => true,
'strategy' => 'all', // 'all', 'include', 'exclude', 'none'
'include' => [],
'exclude' => [],
],
];
PHP;
File::put("{$this->modulePath}/Config/{$this->snakeName}.php", $stub);
$this->info("✓ Created Config");
}
protected function createPermissions(): void
{
$stub = <<<PHP
<?php
return [
'{$this->snakeName}.view' => 'View {$this->studlyName}',
'{$this->snakeName}.create' => 'Create {$this->studlyName} records',
'{$this->snakeName}.edit' => 'Edit {$this->studlyName} records',
'{$this->snakeName}.delete' => 'Delete {$this->studlyName} records',
];
PHP;
File::put("{$this->modulePath}/Permissions.php", $stub);
$this->info("✓ Created Permissions");
}
protected function createController(): void
{
$stub = <<<PHP
<?php
namespace App\Modules\\{$this->studlyName}\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class {$this->studlyName}Controller extends Controller
{
public function index()
{
\$this->authorize('{$this->snakeName}.view');
return view('{$this->kebabName}::index');
}
}
PHP;
File::put("{$this->modulePath}/Http/Controllers/{$this->studlyName}Controller.php", $stub);
$this->info("✓ Created Controller");
}
protected function createRoutes(): void
{
// Web routes
$webStub = <<<PHP
<?php
use App\Modules\\{$this->studlyName}\Http\Controllers\\{$this->studlyName}Controller;
use Illuminate\Support\Facades\Route;
Route::get('/', [{$this->studlyName}Controller::class, 'index'])->name('index');
PHP;
File::put("{$this->modulePath}/Routes/web.php", $webStub);
// API routes (if requested)
if ($this->option('api')) {
$apiStub = <<<PHP
<?php
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->group(function () {
// API routes here
});
PHP;
File::put("{$this->modulePath}/Routes/api.php", $apiStub);
}
$this->info("✓ Created Routes" . ($this->option('api') ? ' (web + api)' : ''));
}
protected function createViews(): void
{
$stub = <<<BLADE
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('{$this->studlyName}') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
{{ __('{$this->studlyName} module is working!') }}
</div>
</div>
</div>
</div>
</x-app-layout>
BLADE;
File::put("{$this->modulePath}/Resources/views/index.blade.php", $stub);
$this->info("✓ Created Views");
}
protected function createModel(): void
{
$tableName = Str::snake(Str::plural($this->modelName));
$stub = <<<PHP
<?php
namespace App\Modules\\{$this->studlyName}\Models;
use App\Traits\ModuleAuditable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use OwenIt\Auditing\Contracts\Auditable;
class {$this->modelName} extends Model implements Auditable
{
use HasFactory, ModuleAuditable;
protected \$table = '{$tableName}';
protected \$fillable = [
'name',
'description',
];
protected \$casts = [
//
];
}
PHP;
File::put("{$this->modulePath}/Models/{$this->modelName}.php", $stub);
$this->info("✓ Created Model: {$this->modelName}");
}
protected function createMigration(): void
{
$tableName = Str::snake(Str::plural($this->modelName));
$timestamp = date('Y_m_d_His');
$migrationName = "create_{$tableName}_table";
$stub = <<<PHP
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('{$tableName}', function (Blueprint \$table) {
\$table->id();
\$table->string('name');
\$table->text('description')->nullable();
\$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('{$tableName}');
}
};
PHP;
$migrationPath = "{$this->modulePath}/Database/Migrations/{$timestamp}_{$migrationName}.php";
File::put($migrationPath, $stub);
$this->info("✓ Created Migration: {$migrationName}");
}
protected function createFilamentResource(): void
{
$modelClass = "App\\Modules\\{$this->studlyName}\\Models\\{$this->modelName}";
$tableName = Str::snake(Str::plural($this->modelName));
// Create the resource
$resourceStub = <<<PHP
<?php
namespace App\Modules\\{$this->studlyName}\Filament\Resources;
use App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource\Pages;
use App\Modules\\{$this->studlyName}\Models\\{$this->modelName};
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
class {$this->modelName}Resource extends Resource
{
protected static ?string \$model = {$this->modelName}::class;
protected static ?string \$navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string \$navigationGroup = '{$this->studlyName}';
protected static ?int \$navigationSort = 1;
public static function canAccess(): bool
{
return auth()->user()?->can('{$this->snakeName}.view') ?? false;
}
public static function form(Form \$form): Form
{
return \$form
->schema([
Forms\Components\Section::make('{$this->modelName} Details')
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\Textarea::make('description')
->rows(3),
]),
]);
}
public static function table(Table \$table): Table
{
return \$table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('description')
->limit(50),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\List{$this->modelName}s::route('/'),
'create' => Pages\Create{$this->modelName}::route('/create'),
'edit' => Pages\Edit{$this->modelName}::route('/{record}/edit'),
];
}
}
PHP;
File::put("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource.php", $resourceStub);
// Create Pages directory
File::makeDirectory("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource/Pages", 0755, true);
// Create List page
$listStub = <<<PHP
<?php
namespace App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource\Pages;
use App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class List{$this->modelName}s extends ListRecords
{
protected static string \$resource = {$this->modelName}Resource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
PHP;
File::put("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource/Pages/List{$this->modelName}s.php", $listStub);
// Create Create page
$createStub = <<<PHP
<?php
namespace App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource\Pages;
use App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource;
use Filament\Resources\Pages\CreateRecord;
class Create{$this->modelName} extends CreateRecord
{
protected static string \$resource = {$this->modelName}Resource::class;
}
PHP;
File::put("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource/Pages/Create{$this->modelName}.php", $createStub);
// Create Edit page
$editStub = <<<PHP
<?php
namespace App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource\Pages;
use App\Modules\\{$this->studlyName}\Filament\Resources\\{$this->modelName}Resource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class Edit{$this->modelName} extends EditRecord
{
protected static string \$resource = {$this->modelName}Resource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}
PHP;
File::put("{$this->modulePath}/Filament/Resources/{$this->modelName}Resource/Pages/Edit{$this->modelName}.php", $editStub);
$this->info("✓ Created Filament Resource: {$this->modelName}Resource");
}
protected function createTests(): void
{
$testPath = base_path("tests/Feature/Modules/{$this->studlyName}");
File::makeDirectory($testPath, 0755, true);
$stub = <<<PHP
<?php
namespace Tests\Feature\Modules\\{$this->studlyName};
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class {$this->studlyName}Test extends TestCase
{
use RefreshDatabase;
public function test_user_can_view_{$this->snakeName}_index(): void
{
\$user = User::factory()->create();
\$user->givePermissionTo('{$this->snakeName}.view');
\$response = \$this->actingAs(\$user)
->get('/{$this->kebabName}');
\$response->assertStatus(200);
}
public function test_unauthorized_user_cannot_view_{$this->snakeName}(): void
{
\$user = User::factory()->create();
\$response = \$this->actingAs(\$user)
->get('/{$this->kebabName}');
\$response->assertStatus(403);
}
}
PHP;
File::put("{$testPath}/{$this->studlyName}Test.php", $stub);
$this->info("✓ Created Tests");
}
protected function registerServiceProvider(): void
{
$providersPath = base_path('bootstrap/providers.php');
if (File::exists($providersPath)) {
$content = File::get($providersPath);
$providerClass = "App\\Modules\\{$this->studlyName}\\{$this->studlyName}ServiceProvider::class";
if (!str_contains($content, $providerClass)) {
// Add the provider before the closing bracket
$content = preg_replace(
'/(\];)/',
" {$providerClass},\n$1",
$content
);
File::put($providersPath, $content);
$this->info("✓ Registered ServiceProvider in bootstrap/providers.php");
}
} else {
$this->warn("⚠ Could not auto-register ServiceProvider. Add manually to config/app.php:");
$this->line(" App\\Modules\\{$this->studlyName}\\{$this->studlyName}ServiceProvider::class");
}
}
}

View File

@@ -0,0 +1,186 @@
<?php
namespace App\Filament\Pages;
use App\Services\ModuleDiscoveryService;
use App\Services\ModuleGeneratorService;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Support\Str;
class ModuleGenerator extends Page implements HasForms
{
use InteractsWithForms;
protected static ?string $navigationIcon = 'heroicon-o-cube';
protected static ?string $navigationLabel = 'Module Generator';
protected static ?string $navigationGroup = 'Development';
protected static ?int $navigationSort = 100;
protected static string $view = 'filament.pages.module-generator';
public ?array $data = [];
public ?array $result = null;
public array $modules = [];
public array $summary = [];
public ?string $expandedModule = null;
public static function canAccess(): bool
{
// Only available in local environment
return app()->environment('local');
}
public static function shouldRegisterNavigation(): bool
{
return app()->environment('local');
}
public function mount(): void
{
$this->form->fill([
'create_git_branch' => true,
'include_api' => false,
]);
$this->loadModules();
}
public function loadModules(): void
{
$discovery = new ModuleDiscoveryService();
$this->modules = $discovery->discoverModules();
$this->summary = $discovery->getModuleSummary();
}
public function toggleModule(string $moduleName): void
{
$this->expandedModule = $this->expandedModule === $moduleName ? null : $moduleName;
}
public function form(Form $form): Form
{
return $form
->schema([
Section::make('Module Details')
->description('Create a new module skeleton with all the necessary folders and files.')
->schema([
TextInput::make('name')
->label('Module Name')
->placeholder('e.g., Accounting, Inventory, HumanResources')
->helperText('Use PascalCase. This will create app/Modules/YourModuleName/')
->required()
->maxLength(50)
->rules(['regex:/^[A-Z][a-zA-Z0-9]*$/'])
->validationMessages([
'regex' => 'Module name must be PascalCase (start with uppercase, letters and numbers only).',
])
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) =>
$set('name', Str::studly($state))
),
Textarea::make('description')
->label('Description')
->placeholder('Brief description of what this module does...')
->rows(2)
->maxLength(255),
]),
Section::make('Options')
->schema([
Toggle::make('create_git_branch')
->label('Create Git Branch')
->helperText('Automatically create and checkout a new branch: module/{module-name}')
->default(true),
Toggle::make('include_api')
->label('Include API Routes')
->helperText('Add Routes/api.php with Sanctum authentication')
->default(false),
])
->columns(2),
Section::make('What Will Be Created')
->schema([
\Filament\Forms\Components\Placeholder::make('structure')
->label('')
->content(fn () => new \Illuminate\Support\HtmlString('
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono bg-gray-50 dark:bg-gray-900 p-4 rounded-lg">
<div class="font-bold mb-2">app/Modules/{ModuleName}/</div>
<div class="ml-4">
├── Config/{module_name}.php<br>
├── Database/Migrations/<br>
├── Database/Seeders/<br>
├── Filament/Resources/<br>
├── Http/Controllers/{ModuleName}Controller.php<br>
├── Http/Middleware/<br>
├── Http/Requests/<br>
├── Models/<br>
├── Policies/<br>
├── Services/<br>
├── Routes/web.php<br>
├── Resources/views/index.blade.php<br>
├── Permissions.php<br>
├── {ModuleName}ServiceProvider.php<br>
└── README.md
</div>
</div>
')),
])
->collapsible()
->collapsed(),
])
->statePath('data');
}
public function generate(): void
{
$data = $this->form->getState();
$service = new ModuleGeneratorService();
$this->result = $service->generate($data['name'], [
'description' => $data['description'] ?? '',
'create_git_branch' => $data['create_git_branch'] ?? true,
'include_api' => $data['include_api'] ?? false,
]);
if ($this->result['success']) {
Notification::make()
->title('Module Created Successfully!')
->body("Module {$this->result['module_name']} has been created.")
->success()
->persistent()
->send();
// Reset form for next module
$this->form->fill([
'name' => '',
'description' => '',
'create_git_branch' => true,
'include_api' => false,
]);
// Reload modules list
$this->loadModules();
} else {
Notification::make()
->title('Module Creation Failed')
->body($this->result['message'])
->danger()
->persistent()
->send();
}
}
public function clearResult(): void
{
$this->result = null;
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace App\Filament\Pages;
use App\Models\Setting;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Pages\Page;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
class Settings extends Page
{
protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
protected static string $view = 'filament.pages.settings';
protected static ?string $navigationGroup = 'Settings';
protected static ?int $navigationSort = 99;
public ?array $data = [];
public function mount(): void
{
$siteName = Setting::get('site_name', config('app.name'));
$siteLogo = Setting::get('site_logo');
$primaryColor = Setting::get('primary_color', '#3b82f6');
$secondaryColor = Setting::get('secondary_color', '#8b5cf6');
$accentColor = Setting::get('accent_color', '#10b981');
$siteDescription = Setting::get('site_description');
$contactEmail = Setting::get('contact_email');
$maintenanceMode = Setting::get('maintenance_mode', false);
$enableRegistration = Setting::get('enable_registration', false);
$this->form->fill([
'site_name' => is_string($siteName) ? $siteName : config('app.name'),
'site_logo' => is_string($siteLogo) || is_null($siteLogo) ? $siteLogo : null,
'primary_color' => is_string($primaryColor) ? $primaryColor : '#3b82f6',
'secondary_color' => is_string($secondaryColor) ? $secondaryColor : '#8b5cf6',
'accent_color' => is_string($accentColor) ? $accentColor : '#10b981',
'site_description' => is_string($siteDescription) || is_null($siteDescription) ? $siteDescription : '',
'contact_email' => is_string($contactEmail) || is_null($contactEmail) ? $contactEmail : '',
'maintenance_mode' => is_bool($maintenanceMode) ? $maintenanceMode : false,
'enable_registration' => is_bool($enableRegistration) ? $enableRegistration : false,
]);
}
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Section::make('General Settings')
->schema([
Forms\Components\TextInput::make('site_name')
->label('Site Name')
->required()
->maxLength(255),
Forms\Components\FileUpload::make('site_logo')
->label('Site Logo')
->image()
->directory('logos')
->visibility('public'),
Forms\Components\Textarea::make('site_description')
->label('Site Description')
->rows(3)
->maxLength(500),
Forms\Components\TextInput::make('contact_email')
->label('Contact Email')
->email()
->maxLength(255),
]),
Forms\Components\Section::make('Color Scheme')
->schema([
Forms\Components\ColorPicker::make('primary_color')
->label('Primary Color'),
Forms\Components\ColorPicker::make('secondary_color')
->label('Secondary Color'),
Forms\Components\ColorPicker::make('accent_color')
->label('Accent Color'),
])
->columns(3),
Forms\Components\Section::make('System')
->schema([
Forms\Components\Toggle::make('enable_registration')
->label('Enable User Registration')
->helperText('Allow new users to register. When disabled, only admins can create users.'),
Forms\Components\Toggle::make('maintenance_mode')
->label('Maintenance Mode')
->helperText('Enable to put the site in maintenance mode'),
]),
])
->statePath('data');
}
protected function getFormActions(): array
{
return [
Action::make('save')
->label('Save Settings')
->submit('save'),
];
}
public function save(): void
{
$data = $this->form->getState();
Setting::set('site_name', $data['site_name'] ?? '');
Setting::set('site_logo', $data['site_logo'] ?? '');
Setting::set('primary_color', $data['primary_color'] ?? '#3b82f6');
Setting::set('secondary_color', $data['secondary_color'] ?? '#8b5cf6');
Setting::set('accent_color', $data['accent_color'] ?? '#10b981');
Setting::set('site_description', $data['site_description'] ?? '');
Setting::set('contact_email', $data['contact_email'] ?? '');
Setting::set('maintenance_mode', $data['maintenance_mode'] ?? false, 'boolean');
Setting::set('enable_registration', $data['enable_registration'] ?? false, 'boolean');
Notification::make()
->title('Settings saved successfully')
->success()
->send();
}
}

View File

@@ -0,0 +1,231 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\AuditResource\Pages;
use App\Models\Audit;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Infolists;
use Filament\Infolists\Infolist;
use Illuminate\Database\Eloquent\Builder;
class AuditResource extends Resource
{
protected static ?string $model = Audit::class;
protected static ?string $navigationIcon = 'heroicon-o-clipboard-document-list';
protected static ?string $navigationGroup = 'System';
protected static ?string $navigationLabel = 'Audit Trail';
protected static ?int $navigationSort = 100;
protected static ?string $modelLabel = 'Audit Log';
protected static ?string $pluralModelLabel = 'Audit Trail';
public static function canAccess(): bool
{
return auth()->user()?->can('audit.view') ?? false;
}
public static function canCreate(): bool
{
return false;
}
public static function canEdit($record): bool
{
return false;
}
public static function canDelete($record): bool
{
return false;
}
public static function form(Form $form): Form
{
return $form->schema([]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('created_at')
->label('Date/Time')
->dateTime('M j, Y H:i:s')
->sortable(),
Tables\Columns\TextColumn::make('user.name')
->label('User')
->default('System')
->searchable()
->sortable(),
Tables\Columns\BadgeColumn::make('event')
->label('Action')
->colors([
'success' => 'created',
'warning' => 'updated',
'danger' => 'deleted',
'info' => 'restored',
])
->formatStateUsing(fn (string $state): string => ucfirst($state)),
Tables\Columns\TextColumn::make('auditable_type')
->label('Model')
->formatStateUsing(fn (string $state): string => class_basename($state))
->sortable(),
Tables\Columns\TextColumn::make('auditable_id')
->label('Record ID')
->sortable(),
Tables\Columns\TextColumn::make('ip_address')
->label('IP Address')
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('url')
->label('URL')
->limit(30)
->toggleable(isToggledHiddenByDefault: true),
])
->defaultSort('created_at', 'desc')
->filters([
Tables\Filters\SelectFilter::make('event')
->label('Action')
->options([
'created' => 'Created',
'updated' => 'Updated',
'deleted' => 'Deleted',
'restored' => 'Restored',
]),
Tables\Filters\SelectFilter::make('user_id')
->label('User')
->relationship('user', 'name')
->searchable()
->preload(),
Tables\Filters\SelectFilter::make('auditable_type')
->label('Model')
->options(fn () => Audit::query()
->distinct()
->pluck('auditable_type')
->mapWithKeys(fn ($type) => [$type => class_basename($type)])
->toArray()
),
Tables\Filters\Filter::make('created_at')
->form([
Forms\Components\DatePicker::make('from')
->label('From Date'),
Forms\Components\DatePicker::make('until')
->label('Until Date'),
])
->query(function (Builder $query, array $data): Builder {
return $query
->when(
$data['from'],
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '>=', $date),
)
->when(
$data['until'],
fn (Builder $query, $date): Builder => $query->whereDate('created_at', '<=', $date),
);
})
->indicateUsing(function (array $data): array {
$indicators = [];
if ($data['from'] ?? null) {
$indicators['from'] = 'From: ' . $data['from'];
}
if ($data['until'] ?? null) {
$indicators['until'] = 'Until: ' . $data['until'];
}
return $indicators;
}),
])
->actions([
Tables\Actions\ViewAction::make(),
])
->bulkActions([])
->poll('60s');
}
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Infolists\Components\Section::make('Audit Details')
->schema([
Infolists\Components\Grid::make(3)
->schema([
Infolists\Components\TextEntry::make('created_at')
->label('Date/Time')
->dateTime('F j, Y H:i:s'),
Infolists\Components\TextEntry::make('user.name')
->label('User')
->default('System'),
Infolists\Components\TextEntry::make('event')
->label('Action')
->badge()
->color(fn (string $state): string => match ($state) {
'created' => 'success',
'updated' => 'warning',
'deleted' => 'danger',
'restored' => 'info',
default => 'gray',
})
->formatStateUsing(fn (string $state): string => ucfirst($state)),
]),
Infolists\Components\Grid::make(3)
->schema([
Infolists\Components\TextEntry::make('auditable_type')
->label('Model')
->formatStateUsing(fn (string $state): string => class_basename($state)),
Infolists\Components\TextEntry::make('auditable_id')
->label('Record ID'),
Infolists\Components\TextEntry::make('ip_address')
->label('IP Address')
->default('N/A'),
]),
Infolists\Components\TextEntry::make('url')
->label('URL')
->columnSpanFull(),
]),
Infolists\Components\Section::make('Changes')
->schema([
Infolists\Components\ViewEntry::make('changes')
->view('filament.infolists.entries.audit-changes')
->columnSpanFull(),
]),
]);
}
public static function getRelations(): array
{
return [];
}
public static function getPages(): array
{
return [
'index' => Pages\ListAudits::route('/'),
'view' => Pages\ViewAudit::route('/{record}'),
];
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Filament\Resources\AuditResource\Pages;
use App\Filament\Resources\AuditResource;
use Filament\Resources\Pages\ListRecords;
class ListAudits extends ListRecords
{
protected static string $resource = AuditResource::class;
protected function getHeaderActions(): array
{
return [];
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Filament\Resources\AuditResource\Pages;
use App\Filament\Resources\AuditResource;
use Filament\Resources\Pages\ViewRecord;
class ViewAudit extends ViewRecord
{
protected static string $resource = AuditResource::class;
protected function getHeaderActions(): array
{
return [];
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\MenuResource\Pages;
use App\Filament\Resources\MenuResource\RelationManagers;
use App\Models\Menu;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\Str;
class MenuResource extends Resource
{
protected static ?string $model = Menu::class;
protected static ?string $navigationIcon = 'heroicon-o-bars-3';
protected static ?string $navigationGroup = 'Settings';
protected static ?int $navigationSort = 10;
public static function canAccess(): bool
{
$user = auth()->user();
return $user?->hasRole('admin') || $user?->can('menus.view');
}
public static function canCreate(): bool
{
$user = auth()->user();
return $user?->hasRole('admin') || $user?->can('menus.create');
}
public static function canEdit($record): bool
{
$user = auth()->user();
return $user?->hasRole('admin') || $user?->can('menus.edit');
}
public static function canDelete($record): bool
{
$user = auth()->user();
return $user?->hasRole('admin') || $user?->can('menus.delete');
}
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Section::make('Menu Details')
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(fn ($state, Forms\Set $set) =>
$set('slug', Str::slug($state))
),
Forms\Components\TextInput::make('slug')
->required()
->maxLength(255)
->unique(ignoreRecord: true),
Forms\Components\Select::make('location')
->options([
'header' => 'Header Navigation',
'footer' => 'Footer Navigation',
'sidebar' => 'Sidebar Navigation',
])
->placeholder('Select a location'),
Forms\Components\Toggle::make('is_active')
->label('Active')
->default(true),
])->columns(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('slug')
->searchable()
->badge()
->color('gray'),
Tables\Columns\TextColumn::make('location')
->badge()
->color(fn (?string $state) => match ($state) {
'header' => 'success',
'footer' => 'warning',
'sidebar' => 'info',
default => 'gray',
}),
Tables\Columns\TextColumn::make('allItems_count')
->label('Items')
->counts('allItems')
->badge(),
Tables\Columns\IconColumn::make('is_active')
->label('Active')
->boolean(),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Tables\Filters\SelectFilter::make('location')
->options([
'header' => 'Header',
'footer' => 'Footer',
'sidebar' => 'Sidebar',
]),
Tables\Filters\TernaryFilter::make('is_active')
->label('Active'),
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
RelationManagers\ItemsRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListMenus::route('/'),
'create' => Pages\CreateMenu::route('/create'),
'edit' => Pages\EditMenu::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\MenuResource\Pages;
use App\Filament\Resources\MenuResource;
use Filament\Resources\Pages\CreateRecord;
class CreateMenu extends CreateRecord
{
protected static string $resource = MenuResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\MenuResource\Pages;
use App\Filament\Resources\MenuResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditMenu extends EditRecord
{
protected static string $resource = MenuResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\MenuResource\Pages;
use App\Filament\Resources\MenuResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListMenus extends ListRecords
{
protected static string $resource = MenuResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace App\Filament\Resources\MenuResource\RelationManagers;
use App\Models\MenuItem;
use App\Services\MenuService;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Spatie\Permission\Models\Permission;
class ItemsRelationManager extends RelationManager
{
protected static string $relationship = 'allItems';
protected static ?string $title = 'Menu Items';
public function form(Form $form): Form
{
$menuService = app(MenuService::class);
return $form
->schema([
Forms\Components\Section::make('Item Details')
->schema([
Forms\Components\TextInput::make('title')
->required()
->maxLength(255),
Forms\Components\Select::make('type')
->options([
'link' => 'External Link',
'route' => 'Named Route',
'module' => 'Module',
])
->default('link')
->required()
->live(),
Forms\Components\TextInput::make('url')
->label('URL')
->url()
->visible(fn (Forms\Get $get) => $get('type') === 'link')
->required(fn (Forms\Get $get) => $get('type') === 'link'),
Forms\Components\Select::make('route')
->label('Route Name')
->options(fn () => $menuService->getAvailableRoutes())
->searchable()
->visible(fn (Forms\Get $get) => $get('type') === 'route')
->required(fn (Forms\Get $get) => $get('type') === 'route'),
Forms\Components\Select::make('module')
->label('Module')
->options(fn () => $menuService->getAvailableModules())
->searchable()
->visible(fn (Forms\Get $get) => $get('type') === 'module')
->required(fn (Forms\Get $get) => $get('type') === 'module')
->helperText('Users need view permission for this module to see this item'),
])->columns(2),
Forms\Components\Section::make('Permissions & Display')
->schema([
Forms\Components\Select::make('parent_id')
->label('Parent Item')
->options(fn () => MenuItem::where('menu_id', $this->ownerRecord->id)
->whereNull('parent_id')
->pluck('title', 'id'))
->placeholder('None (Top Level)')
->searchable(),
Forms\Components\Select::make('permission')
->label('Required Permission')
->options(fn () => Permission::pluck('name', 'name'))
->searchable()
->placeholder('No specific permission required')
->helperText('If set, user must have this permission to see this item'),
Forms\Components\TextInput::make('icon')
->placeholder('heroicon-o-home')
->helperText('Heroicon name or custom icon class'),
Forms\Components\Select::make('target')
->options([
'_self' => 'Same Window',
'_blank' => 'New Tab',
])
->default('_self'),
Forms\Components\TextInput::make('order')
->numeric()
->default(0)
->helperText('Lower numbers appear first'),
Forms\Components\Toggle::make('is_active')
->label('Active')
->default(true),
])->columns(2),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('title')
->reorderable('order')
->defaultSort('order')
->columns([
Tables\Columns\TextColumn::make('title')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('parent.title')
->label('Parent')
->placeholder('—')
->badge()
->color('gray'),
Tables\Columns\TextColumn::make('type')
->badge()
->color(fn (string $state) => match ($state) {
'link' => 'info',
'route' => 'success',
'module' => 'warning',
default => 'gray',
}),
Tables\Columns\TextColumn::make('module')
->placeholder('—')
->toggleable(),
Tables\Columns\TextColumn::make('permission')
->placeholder('—')
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('order')
->sortable(),
Tables\Columns\IconColumn::make('is_active')
->label('Active')
->boolean(),
])
->filters([
Tables\Filters\SelectFilter::make('type')
->options([
'link' => 'External Link',
'route' => 'Named Route',
'module' => 'Module',
]),
Tables\Filters\TernaryFilter::make('is_active')
->label('Active'),
])
->headerActions([
Tables\Actions\CreateAction::make(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\PermissionResource\Pages;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Spatie\Permission\Models\Permission;
class PermissionResource extends Resource
{
protected static ?string $model = Permission::class;
protected static ?string $navigationIcon = 'heroicon-o-key';
protected static ?string $navigationGroup = 'User Management';
protected static ?int $navigationSort = 3;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Section::make('Permission Details')
->schema([
Forms\Components\TextInput::make('name')
->required()
->unique(ignoreRecord: true)
->maxLength(255)
->helperText('Use format: resource.action (e.g., users.create, posts.edit)'),
Forms\Components\Select::make('guard_name')
->options([
'web' => 'Web',
'api' => 'API',
])
->default('web')
->required(),
])->columns(2),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->badge()
->color('info')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('guard_name')
->badge()
->color('gray'),
Tables\Columns\TextColumn::make('roles_count')
->counts('roles')
->label('Roles')
->badge()
->color('success'),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListPermissions::route('/'),
'create' => Pages\CreatePermission::route('/create'),
'edit' => Pages\EditPermission::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\PermissionResource\Pages;
use App\Filament\Resources\PermissionResource;
use Filament\Resources\Pages\CreateRecord;
class CreatePermission extends CreateRecord
{
protected static string $resource = PermissionResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\PermissionResource\Pages;
use App\Filament\Resources\PermissionResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditPermission extends EditRecord
{
protected static string $resource = PermissionResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\PermissionResource\Pages;
use App\Filament\Resources\PermissionResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListPermissions extends ListRecords
{
protected static string $resource = PermissionResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\RoleResource\Pages;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Spatie\Permission\Models\Role;
class RoleResource extends Resource
{
protected static ?string $model = Role::class;
protected static ?string $navigationIcon = 'heroicon-o-shield-check';
protected static ?string $navigationGroup = 'User Management';
protected static ?int $navigationSort = 2;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Section::make('Role Details')
->schema([
Forms\Components\TextInput::make('name')
->required()
->unique(ignoreRecord: true)
->maxLength(255),
Forms\Components\Select::make('guard_name')
->options([
'web' => 'Web',
'api' => 'API',
])
->default('web')
->required(),
])->columns(2),
Forms\Components\Section::make('Permissions')
->schema([
Forms\Components\CheckboxList::make('permissions')
->relationship('permissions', 'name')
->columns(3)
->helperText('Select permissions for this role'),
]),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->badge()
->color(fn (string $state): string => match ($state) {
'admin' => 'danger',
'editor' => 'warning',
default => 'success',
})
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('guard_name')
->badge()
->color('gray'),
Tables\Columns\TextColumn::make('permissions_count')
->counts('permissions')
->label('Permissions')
->badge()
->color('info'),
Tables\Columns\TextColumn::make('users_count')
->counts('users')
->label('Users')
->badge()
->color('success'),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make()
->before(function (Role $record) {
if ($record->name === 'admin') {
throw new \Exception('Cannot delete the admin role.');
}
}),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListRoles::route('/'),
'create' => Pages\CreateRole::route('/create'),
'edit' => Pages\EditRole::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\RoleResource\Pages;
use App\Filament\Resources\RoleResource;
use Filament\Resources\Pages\CreateRecord;
class CreateRole extends CreateRecord
{
protected static string $resource = RoleResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\RoleResource\Pages;
use App\Filament\Resources\RoleResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditRole extends EditRecord
{
protected static string $resource = RoleResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\RoleResource\Pages;
use App\Filament\Resources\RoleResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListRoles extends ListRecords
{
protected static string $resource = RoleResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\UserResource\Pages;
use App\Models\User;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Hash;
use Spatie\Permission\Models\Role;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static ?string $navigationIcon = 'heroicon-o-users';
protected static ?string $navigationGroup = 'User Management';
protected static ?int $navigationSort = 1;
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Section::make('User Information')
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('email')
->email()
->required()
->unique(ignoreRecord: true)
->maxLength(255),
Forms\Components\DateTimePicker::make('email_verified_at')
->label('Email Verified At'),
])->columns(2),
Forms\Components\Section::make('Password')
->schema([
Forms\Components\TextInput::make('password')
->password()
->dehydrateStateUsing(fn ($state) => Hash::make($state))
->dehydrated(fn ($state) => filled($state))
->required(fn (string $context): bool => $context === 'create')
->maxLength(255)
->confirmed(),
Forms\Components\TextInput::make('password_confirmation')
->password()
->maxLength(255)
->dehydrated(false),
])->columns(2),
Forms\Components\Section::make('Roles')
->schema([
Forms\Components\CheckboxList::make('roles')
->relationship('roles', 'name')
->columns(3)
->helperText('Assign roles to this user'),
]),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('email')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('roles.name')
->badge()
->color('success')
->separator(','),
Tables\Columns\IconColumn::make('email_verified_at')
->label('Verified')
->boolean()
->trueIcon('heroicon-o-check-circle')
->falseIcon('heroicon-o-x-circle'),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Tables\Filters\SelectFilter::make('roles')
->relationship('roles', 'name')
->multiple()
->preload(),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListUsers::route('/'),
'create' => Pages\CreateUser::route('/create'),
'edit' => Pages\EditUser::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditUser extends EditRecord
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords
{
protected static string $resource = UserResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Helpers;
class ViteHelper
{
/**
* Check if the Vite manifest exists (assets have been built).
*/
public static function manifestExists(): bool
{
return file_exists(public_path('build/manifest.json'));
}
/**
* Get fallback CSS for when Vite assets are not built.
* This provides basic styling so the app remains usable.
*/
public static function fallbackStyles(): string
{
return <<<'CSS'
<style>
/* Fallback styles when Vite assets are not built */
*, *::before, *::after { box-sizing: border-box; }
body {
font-family: ui-sans-serif, system-ui, sans-serif;
margin: 0;
background: #f3f4f6;
color: #111827;
}
.dark body { background: #111827; color: #f9fafb; }
.min-h-screen { min-height: 100vh; }
.bg-gray-100 { background: #f3f4f6; }
.bg-white { background: #fff; }
.shadow { box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.max-w-7xl { max-width: 80rem; margin: 0 auto; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
.font-semibold { font-weight: 600; }
.text-xl { font-size: 1.25rem; }
.text-gray-800 { color: #1f2937; }
a { color: #3b82f6; text-decoration: none; }
a:hover { text-decoration: underline; }
.hidden { display: none; }
.flex { display: flex; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.space-x-4 > * + * { margin-left: 1rem; }
nav { background: #fff; border-bottom: 1px solid #e5e7eb; padding: 1rem; }
.container { max-width: 80rem; margin: 0 auto; padding: 0 1rem; }
button, .btn {
padding: 0.5rem 1rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 0.375rem;
cursor: pointer;
}
button:hover, .btn:hover { background: #2563eb; }
input, select, textarea {
border: 1px solid #d1d5db;
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
width: 100%;
}
.alert { padding: 1rem; border-radius: 0.375rem; margin-bottom: 1rem; }
.alert-warning { background: #fef3c7; border: 1px solid #f59e0b; color: #92400e; }
</style>
CSS;
}
/**
* Get a warning banner HTML for development.
*/
public static function devWarningBanner(): string
{
if (app()->environment('production')) {
return '';
}
return <<<'HTML'
<div class="alert alert-warning" style="margin:1rem;padding:1rem;background:#fef3c7;border:1px solid #f59e0b;border-radius:0.5rem;color:#92400e;">
<strong>⚠️ Development Notice:</strong> Vite assets are not built.
Run <code style="background:#fde68a;padding:0.125rem 0.25rem;border-radius:0.25rem;">docker-compose run --rm node npm run build</code> to build assets.
</div>
HTML;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): View
{
return view('auth.login');
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): View
{
return view('auth.confirm-password');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(route('dashboard', absolute: false));
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|View
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: view('auth.verify-email');
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): View
{
return view('auth.reset-password', ['request' => $request]);
}
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function (User $user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $status == Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): View
{
return view('auth.forgot-password');
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
return $status == Password::RESET_LINK_SENT
? back()->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class RegisteredUserController extends Controller
{
/**
* Display the registration view.
*/
public function create(): View
{
return view('auth.register');
}
/**
* Handle an incoming registration request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(route('dashboard', absolute: false));
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ProfileUpdateRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\View\View;
class ProfileController extends Controller
{
/**
* Display the user's profile form.
*/
public function edit(Request $request): View
{
return view('profile.edit', [
'user' => $request->user(),
]);
}
/**
* Update the user's profile information.
*/
public function update(ProfileUpdateRequest $request): RedirectResponse
{
$request->user()->fill($request->validated());
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
}
$request->user()->save();
return Redirect::route('profile.edit')->with('status', 'profile-updated');
}
/**
* Delete the user's account.
*/
public function destroy(Request $request): RedirectResponse
{
$request->validateWithBag('userDeletion', [
'password' => ['required', 'current_password'],
]);
$user = $request->user();
Auth::logout();
$user->delete();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use App\Models\Setting;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class CheckRegistrationEnabled
{
/**
* Handle an incoming request.
* Block registration routes if registration is disabled in settings.
*/
public function handle(Request $request, Closure $next): Response
{
$registrationEnabled = Setting::get('enable_registration', false);
if (!$registrationEnabled) {
abort(403, 'User registration is currently disabled. Please contact an administrator.');
}
return $next($request);
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ProfileUpdateRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'lowercase',
'email',
'max:255',
Rule::unique(User::class)->ignore($this->user()->id),
],
];
}
}

Some files were not shown because too many files have changed in this diff Show More