commit 17dbdad28bcc18cdf628dcf117dd81b2ad3bf20d Author: theRADcozaDEV Date: Fri Mar 6 08:57:05 2026 +0200 templave v1 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..87cf57b --- /dev/null +++ b/.env.example @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..27a05a3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59c99c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Environment files +.env +!.env.example + +# Docker volumes +mysql_data/ +redis_data/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Laravel (if src is committed) +src/vendor/ +src/node_modules/ +src/.env +src/storage/*.key +src/storage/logs/* +!src/storage/logs/.gitkeep diff --git a/.windsurf/workflows/js.md b/.windsurf/workflows/js.md new file mode 100644 index 0000000..e69de29 diff --git a/AI_CONTEXT.md b/AI_CONTEXT.md new file mode 100644 index 0000000..ecb228b --- /dev/null +++ b/AI_CONTEXT.md @@ -0,0 +1,299 @@ +# AI Assistant Context + +This document provides context for AI coding assistants working on projects built with this template. + +## Template Overview + +This is a **Laravel Docker Development Template** with: +- Docker-based development environment +- Production deployment to Ubuntu 24.04 (no Docker) +- Modular architecture with Filament admin +- Audit trail for all data changes +- Multi-database support (MySQL, PostgreSQL, SQLite) + +## 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 or Jetstream (Livewire only) | +| **Permissions** | spatie/laravel-permission | +| **Audit** | owen-it/laravel-auditing | +| **Error Tracking** | spatie/laravel-flare + spatie/laravel-ignition | +| **Code Style** | Laravel Pint | + +## 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 + +- [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 diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md new file mode 100644 index 0000000..8d83411 --- /dev/null +++ b/GETTING_STARTED.md @@ -0,0 +1,317 @@ +# Getting Started + +This guide walks you through setting up a new Laravel project using this template. + +## Prerequisites + +- Docker & Docker Compose +- Git +- Make (optional, but recommended) + +## Quick Start (5 minutes) + +```bash +# 1. Clone the template +git clone https://github.com/your-repo/Laravel-Docker-Dev-Template.git my-project +cd my-project + +# 2. Copy environment file +cp .env.example .env + +# 3. Choose your database and start +make install DB=mysql # or: pgsql, sqlite + +# 4. Run setup scripts (interactive) +make setup-tools # Flare, Pint, error pages +make setup-laravel # Auth, Filament, modules, audit trail + +# 5. Access your app +# Laravel: http://localhost:8080 +# Admin: http://localhost:8080/admin +# Mail: http://localhost:8025 +``` + +## Step-by-Step Setup + +### 1. Clone and Configure + +```bash +git clone https://github.com/your-repo/Laravel-Docker-Dev-Template.git my-project +cd my-project + +# Remove template git history and start fresh +rm -rf .git +git init +``` + +### 2. Choose Database + +| Database | Best For | Command | +|----------|----------|---------| +| **MySQL** | Most projects, production parity | `make install DB=mysql` | +| **PostgreSQL** | Advanced features, JSON, full-text search | `make install DB=pgsql` | +| **SQLite** | Simple apps, quick prototyping | `make install DB=sqlite` | + +### 3. Install Laravel + +```bash +# Start containers and create Laravel project +make install DB=mysql + +# This will: +# - Build Docker images +# - Start containers +# - Run composer create-project laravel/laravel +# - Copy appropriate .env file +# - Generate app key +# - Run initial migrations +``` + +### 4. Run Setup Scripts + +#### Post-Install Tools + +```bash +make setup-tools +``` + +Installs: +- ✅ Spatie Ignition (dev error pages) +- ✅ Spatie Flare (production error tracking) +- ✅ Laravel Pint (code style) +- ✅ Custom error pages (404, 500, 503) +- ❓ Laravel Telescope (optional debugging) + +#### Laravel Base Setup + +```bash +make setup-laravel +``` + +Interactive prompts for: + +1. **Authentication** - Choose one: + - Breeze + Blade (recommended) + - Breeze + Livewire + - Breeze API only + - Jetstream + Livewire + +2. **Filament Admin** - User management dashboard + +3. **Audit Trail** - Track all data changes + +4. **Module System** - Modular architecture + +5. **API (Sanctum)** - Token authentication + +6. **Security Middleware** - HTTPS, headers + +### 5. Create Your First Module + +```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 module +php artisan make:module ModuleName + +# With model +php artisan make:module ModuleName --model=ModelName + +# With API +php artisan make:module ModuleName --api + +# Without Filament +php artisan make:module ModuleName --no-filament +``` + +## 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 + +### 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 Flare** - Get API key at [flareapp.io](https://flareapp.io) +2. **Create modules** - `php artisan make:module YourFeature` +3. **Add models** - `php artisan make:module YourFeature --model=YourModel` +4. **Write tests** - `make test` +5. **Deploy** - See [Production Deployment](README.md#production-deployment) + +## Documentation + +- [README.md](README.md) - Overview and commands +- [docs/laravel-setup.md](docs/laravel-setup.md) - Detailed setup options +- [docs/modules.md](docs/modules.md) - Module architecture +- [docs/audit-trail.md](docs/audit-trail.md) - Audit configuration +- [docs/filament-admin.md](docs/filament-admin.md) - Admin panel +- [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) - CI/CD pipeline +- [docs/backup.md](docs/backup.md) - Database backup diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f810fc9 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..418b7c5 --- /dev/null +++ b/README.md @@ -0,0 +1,540 @@ +# 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 [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 +│ ├── 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 +- Make (optional, for convenience commands) + +### Setup + +1. **Clone the repository** + ```bash + git clone my-laravel-app + cd my-laravel-app + ``` + +2. **Copy environment file** + ```bash + cp .env.example .env + ``` + +3. **Install Laravel with your preferred database** + ```bash + # With MySQL (default) + make install DB=mysql + + # With PostgreSQL + make install DB=pgsql + + # With SQLite + make install DB=sqlite + + # Or without Make: + docker-compose build + docker-compose --profile mysql run --rm app composer create-project laravel/laravel . + cp src/.env.mysql src/.env + ``` + +4. **Start the development environment** + ```bash + # Start with your chosen database + make up DB=mysql # or pgsql, sqlite + + # Or: docker-compose --profile mysql up -d + ``` + +5. **Access your application** + - Laravel App: http://localhost:8080 + - Mailpit: http://localhost:8025 + +6. **Run setup scripts** + ```bash + # Install Flare, Pint, error pages + make setup-tools + + # Configure auth, API, middleware (interactive) + make setup-laravel + + # Or run both + make setup-all + ``` + +### 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 | + +## 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 --}} + + +{{-- Logo --}} +{{ site_name() }} + +{{-- Colors available as CSS variables --}} +
Uses --primary-color
+``` + +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 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 ci && npm run build + ``` + +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 diff --git a/deploy/apache/apache-setup.md b/deploy/apache/apache-setup.md new file mode 100644 index 0000000..0a6100e --- /dev/null +++ b/deploy/apache/apache-setup.md @@ -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 diff --git a/deploy/apache/laravel-site.conf b/deploy/apache/laravel-site.conf new file mode 100644 index 0000000..51b2836 --- /dev/null +++ b/deploy/apache/laravel-site.conf @@ -0,0 +1,115 @@ + + ServerName your-domain.com + ServerAlias www.your-domain.com + ServerAdmin webmaster@your-domain.com + + DocumentRoot /var/www/your-app/public + + + Options -Indexes +FollowSymLinks + AllowOverride All + Require all granted + + + # PHP-FPM configuration + + SetHandler "proxy:unix:/var/run/php/php8.3-fpm.sock|fcgi://localhost" + + + # 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 + + 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 + + + # Static file caching + + 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" + + + +# SSL Configuration (use with Let's Encrypt / Certbot) + + ServerName your-domain.com + ServerAlias www.your-domain.com + ServerAdmin webmaster@your-domain.com + + DocumentRoot /var/www/your-app/public + + + Options -Indexes +FollowSymLinks + AllowOverride All + Require all granted + + + # PHP-FPM configuration + + SetHandler "proxy:unix:/var/run/php/php8.3-fpm.sock|fcgi://localhost" + + + # 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 + + 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 + + + # Static file caching + + 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" + + diff --git a/deploy/nginx/laravel-site.conf b/deploy/nginx/laravel-site.conf new file mode 100644 index 0000000..fb45d03 --- /dev/null +++ b/deploy/nginx/laravel-site.conf @@ -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; +} diff --git a/deploy/nginx/nginx-proxy-manager-notes.md b/deploy/nginx/nginx-proxy-manager-notes.md new file mode 100644 index 0000000..40aca67 --- /dev/null +++ b/deploy/nginx/nginx-proxy-manager-notes.md @@ -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=* +``` diff --git a/deploy/production/.env.mysql.production b/deploy/production/.env.mysql.production new file mode 100644 index 0000000..7680279 --- /dev/null +++ b/deploy/production/.env.mysql.production @@ -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 diff --git a/deploy/production/.env.pgsql.production b/deploy/production/.env.pgsql.production new file mode 100644 index 0000000..b609387 --- /dev/null +++ b/deploy/production/.env.pgsql.production @@ -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 diff --git a/deploy/production/.env.sqlite.production b/deploy/production/.env.sqlite.production new file mode 100644 index 0000000..2b288a6 --- /dev/null +++ b/deploy/production/.env.sqlite.production @@ -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 diff --git a/deploy/scripts/deploy.sh b/deploy/scripts/deploy.sh new file mode 100644 index 0000000..46bbcc2 --- /dev/null +++ b/deploy/scripts/deploy.sh @@ -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 ci --production=false + +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 "==========================================" diff --git a/deploy/scripts/fix-permissions.sh b/deploy/scripts/fix-permissions.sh new file mode 100644 index 0000000..20e867a --- /dev/null +++ b/deploy/scripts/fix-permissions.sh @@ -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!" diff --git a/deploy/scripts/laravel-scheduler.cron b/deploy/scripts/laravel-scheduler.cron new file mode 100644 index 0000000..8c8b0b5 --- /dev/null +++ b/deploy/scripts/laravel-scheduler.cron @@ -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 diff --git a/deploy/scripts/server-setup.sh b/deploy/scripts/server-setup.sh new file mode 100644 index 0000000..f526e0e --- /dev/null +++ b/deploy/scripts/server-setup.sh @@ -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 "" diff --git a/deploy/scripts/supervisor-scheduler.conf b/deploy/scripts/supervisor-scheduler.conf new file mode 100644 index 0000000..ed0da4f --- /dev/null +++ b/deploy/scripts/supervisor-scheduler.conf @@ -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 diff --git a/deploy/scripts/supervisor-worker.conf b/deploy/scripts/supervisor-worker.conf new file mode 100644 index 0000000..d516ce9 --- /dev/null +++ b/deploy/scripts/supervisor-worker.conf @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8c39510 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,173 @@ +version: '3.8' + +services: + # PHP-FPM Application Server + app: + build: + context: . + dockerfile: docker/php/Dockerfile + container_name: laravel_app + restart: unless-stopped + working_dir: /var/www/html + volumes: + - ./src:/var/www/html + - ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini + - sqlite_data:/var/www/html/database + networks: + - laravel_network + depends_on: + - redis + + # Nginx Web Server + nginx: + image: nginx:alpine + container_name: laravel_nginx + restart: unless-stopped + ports: + - "${APP_PORT:-8080}:80" + volumes: + - ./src:/var/www/html + - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf + networks: + - laravel_network + 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 + container_name: laravel_mysql + 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 + container_name: laravel_pgsql + 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 + container_name: laravel_sqlite_init + volumes: + - ./src/database:/data + command: sh -c "touch /data/database.sqlite && chmod 666 /data/database.sqlite" + profiles: + - sqlite + + # Redis Cache + redis: + image: redis:alpine + container_name: laravel_redis + 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 + container_name: laravel_node + 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 + container_name: laravel_queue + restart: unless-stopped + working_dir: /var/www/html + volumes: + - ./src:/var/www/html + - ./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 + container_name: laravel_scheduler + restart: unless-stopped + working_dir: /var/www/html + volumes: + - ./src:/var/www/html + - ./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 + container_name: laravel_mailpit + restart: unless-stopped + ports: + - "${MAIL_PORT:-1025}:1025" + - "${MAIL_DASHBOARD_PORT:-8025}:8025" + networks: + - laravel_network + +networks: + laravel_network: + driver: bridge + +volumes: + mysql_data: + pgsql_data: + redis_data: + sqlite_data: diff --git a/docker/mysql/my.cnf b/docker/mysql/my.cnf new file mode 100644 index 0000000..7b0732d --- /dev/null +++ b/docker/mysql/my.cnf @@ -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 diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..a9de203 --- /dev/null +++ b/docker/nginx/default.conf @@ -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; +} diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 0000000..8739004 --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,57 @@ +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 + +# 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 existing application directory permissions +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 + +USER devuser + +EXPOSE 9000 +CMD ["php-fpm"] diff --git a/docker/php/local.ini b/docker/php/local.ini new file mode 100644 index 0000000..9b10d4f --- /dev/null +++ b/docker/php/local.ini @@ -0,0 +1,15 @@ +; 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 (disabled for development for live reloading) +opcache.enable = 0 +opcache.enable_cli = 0 diff --git a/docs/audit-trail.md b/docs/audit-trail.md new file mode 100644 index 0000000..df712c0 --- /dev/null +++ b/docs/audit-trail.md @@ -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 diff --git a/docs/backup.md b/docs/backup.md new file mode 100644 index 0000000..e0debda --- /dev/null +++ b/docs/backup.md @@ -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 +``` diff --git a/docs/ci-cd.md b/docs/ci-cd.md new file mode 100644 index 0000000..03b8fc1 --- /dev/null +++ b/docs/ci-cd.md @@ -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 ci +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 + +# 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 diff --git a/docs/error-logging.md b/docs/error-logging.md new file mode 100644 index 0000000..2b9d0db --- /dev/null +++ b/docs/error-logging.md @@ -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` diff --git a/docs/filament-admin.md b/docs/filament-admin.md new file mode 100644 index 0000000..0817fd2 --- /dev/null +++ b/docs/filament-admin.md @@ -0,0 +1,220 @@ +# Filament Admin Panel + +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) diff --git a/docs/laravel-setup.md b/docs/laravel-setup.md new file mode 100644 index 0000000..1619115 --- /dev/null +++ b/docs/laravel-setup.md @@ -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 diff --git a/docs/modules.md b/docs/modules.md new file mode 100644 index 0000000..66a5a60 --- /dev/null +++ b/docs/modules.md @@ -0,0 +1,313 @@ +# Modular Architecture + +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') + Stock Management +@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') + +@endpush +``` + +Or inline in the view: +```blade +@push('module-styles') + +@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` diff --git a/docs/queues.md b/docs/queues.md new file mode 100644 index 0000000..e414b00 --- /dev/null +++ b/docs/queues.md @@ -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 +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 +order->id), + ]; + } +} +``` + +## Monitoring + +### View Failed Jobs + +```bash +php artisan queue:failed +``` + +### Retry Failed Jobs + +```bash +# Retry specific job +php artisan queue:retry + +# 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'); +}); +``` diff --git a/docs/site-settings.md b/docs/site-settings.md new file mode 100644 index 0000000..3c83c50 --- /dev/null +++ b/docs/site-settings.md @@ -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 --}} +

{{ site_name() }}

+ +{{-- Logo with fallback --}} +@if(site_logo()) + {{ site_name() }} +@else + {{ site_name() }} +@endif + +{{-- Favicon --}} + + +{{-- Colors --}} +
Primary
+
Secondary
+ +{{-- Get any setting --}} +{{ site_settings('footer_text') }} +{{ site_settings('dark_mode') ? 'Dark' : 'Light' }} +``` + +### Site Head Component + +Include in your layout's ``: + +```blade + + + + + {{-- This adds title, favicon, and CSS variables --}} + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + +``` + +The component automatically: +- Sets the page title with site name +- Adds favicon link +- Creates CSS custom properties for colors + +### CSS Custom Properties + +After including ``, 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 + +``` + +### Utility Classes + +The component also creates utility classes: + +```html +
Primary background
+
Primary text
+
Primary border
+ +
Secondary background
+ +``` + +## 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 + + + + + + + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + +
+ {{-- Header with logo --}} +
+
+ @if(site_logo()) + {{ site_name() }} + @else + {{ site_name() }} + @endif +
+
+ + {{-- Content --}} +
+ {{ $slot }} +
+ + {{-- Footer --}} +
+
+ {!! site_settings('footer_text') ?? '© ' . date('Y') . ' ' . site_name() !!} +
+
+
+ + +``` + +## 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 `` is in your layout's `` +2. Check browser dev tools for CSS variable values diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..666aa03 --- /dev/null +++ b/docs/testing.md @@ -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 +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 diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100644 index 0000000..4d4a651 --- /dev/null +++ b/scripts/backup.sh @@ -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 diff --git a/scripts/laravel-setup.sh b/scripts/laravel-setup.sh new file mode 100644 index 0000000..987fd21 --- /dev/null +++ b/scripts/laravel-setup.sh @@ -0,0 +1,1006 @@ +#!/bin/bash + +# Laravel Base Setup Script +# Run this after post-install.sh to configure auth, API, and base structure +# Usage: ./scripts/laravel-setup.sh + +set -e + +echo "==========================================" +echo "Laravel Base 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 + +# ============================================ +# AUTH SCAFFOLDING (Blade-focused, no JS frameworks) +# ============================================ +echo "" +echo "============================================" +echo "AUTHENTICATION SETUP (Blade-based)" +echo "============================================" +echo "" +echo "Choose authentication scaffolding:" +echo "" +echo "1) Laravel Breeze + Blade (Recommended)" +echo " - Simple, server-side rendered" +echo " - Login, register, password reset, email verification" +echo " - Tailwind CSS styling" +echo "" +echo "2) Laravel Breeze + Livewire" +echo " - Reactive components without writing JavaScript" +echo " - Same features as Blade with dynamic updates" +echo "" +echo "3) Laravel Breeze API only" +echo " - Headless API authentication" +echo " - For when you build your own Blade views" +echo "" +echo "4) Laravel Jetstream + Livewire (Full-featured)" +echo " - Profile management, 2FA, API tokens" +echo " - Optional teams feature" +echo " - Best for SaaS applications" +echo "" +echo "5) None (Manual setup later)" +echo "" +read -p "Enter choice [1-5]: " AUTH_CHOICE + +case $AUTH_CHOICE in + 1) + echo "" + echo "Installing Breeze with Blade..." + composer require laravel/breeze --dev + php artisan breeze:install blade + php artisan migrate + npm install && npm run build + echo "Breeze (Blade) installed successfully!" + ;; + 2) + echo "" + echo "Installing Breeze with Livewire..." + composer require laravel/breeze --dev + php artisan breeze:install livewire + php artisan migrate + npm install && npm run build + echo "Breeze (Livewire) installed successfully!" + ;; + 3) + echo "" + echo "Installing Breeze API only..." + composer require laravel/breeze --dev + php artisan breeze:install api + php artisan migrate + echo "Breeze (API) installed successfully!" + echo "Build your own Blade views for the frontend." + ;; + 4) + echo "" + read -p "Enable Teams feature? [y/N]: " ENABLE_TEAMS + + composer require laravel/jetstream + + TEAMS_FLAG="" + if [[ "$ENABLE_TEAMS" =~ ^[Yy]$ ]]; then + TEAMS_FLAG="--teams" + fi + + php artisan jetstream:install livewire $TEAMS_FLAG + php artisan migrate + npm install && npm run build + echo "Jetstream (Livewire) installed successfully!" + ;; + 5) + echo "Skipping auth scaffolding." + ;; +esac + +# ============================================ +# TESTING (PEST) +# ============================================ +echo "" +echo "============================================" +echo "TESTING FRAMEWORK (Pest)" +echo "============================================" +echo "" +echo "Pest is a testing framework with elegant syntax:" +echo " - Clean, readable test syntax" +echo " - Built on top of PHPUnit" +echo " - Great for unit & feature tests" +echo "" +read -p "Install Pest testing framework? [Y/n]: " INSTALL_PEST + +if [[ ! "$INSTALL_PEST" =~ ^[Nn]$ ]]; then + echo "" + echo "Installing Pest..." + composer require pestphp/pest --dev --with-all-dependencies + composer require pestphp/pest-plugin-laravel --dev + + # Initialize Pest + php artisan pest:install + + # Configure phpunit.xml for SQLite in-memory + if [ -f "phpunit.xml" ]; then + # Add SQLite in-memory for testing + sed -i 's|||' phpunit.xml + sed -i 's|||' phpunit.xml + fi + + # Create tests directory structure + mkdir -p tests/Feature + mkdir -p tests/Unit + mkdir -p tests/Modules + + # Create Pest.php helper with useful traits + cat > tests/Pest.php << 'PEST' +in('Feature', 'Modules'); +uses(Tests\TestCase::class)->in('Unit'); + +/* +|-------------------------------------------------------------------------- +| Database Refresh +|-------------------------------------------------------------------------- +| Uses LazilyRefreshDatabase for faster tests - only migrates when needed. +*/ + +uses(LazilyRefreshDatabase::class)->in('Feature', 'Modules'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +*/ + +function createUser(array $attributes = []): \App\Models\User +{ + return \App\Models\User::factory()->create($attributes); +} + +function createAdmin(array $attributes = []): \App\Models\User +{ + $user = createUser($attributes); + $user->assignRole('admin'); + return $user; +} +PEST + + # Create example feature test + cat > tests/Feature/ExampleTest.php << 'TEST' +get('/'); + + $response->assertStatus(200); +}); + +it('redirects guests from admin to login', function () { + $response = $this->get('/admin'); + + $response->assertRedirect('/admin/login'); +}); +TEST + + # Create example unit test + cat > tests/Unit/ExampleTest.php << 'TEST' +toBeTrue(); + expect(1 + 1)->toBe(2); + expect(['a', 'b', 'c'])->toContain('b'); +}); + +it('can use Laravel helpers', function () { + expect(config('app.name'))->toBeString(); + expect(app())->toBeInstanceOf(\Illuminate\Foundation\Application::class); +}); +TEST + + # Create module test helper + cat > tests/Modules/.gitkeep << 'GITKEEP' +# Module tests go here +# Create subdirectories per module, e.g., tests/Modules/Inventory/ +GITKEEP + + # Create test helper for modules + cat > tests/TestHelpers.php << 'HELPER' +create(); + + foreach ($permissions as $permission) { + $user->givePermissionTo($permission); + } + + return $user; + } + + /** + * Create a user with module access. + */ + protected function userWithModuleAccess(string $moduleSlug): \App\Models\User + { + return $this->userWithPermissions([ + "{$moduleSlug}.view", + "{$moduleSlug}.create", + "{$moduleSlug}.edit", + "{$moduleSlug}.delete", + ]); + } + + /** + * Assert model was audited. + */ + protected function assertAudited($model, string $event = 'created'): void + { + $this->assertDatabaseHas('audits', [ + 'auditable_type' => get_class($model), + 'auditable_id' => $model->id, + 'event' => $event, + ]); + } +} +HELPER + + echo "" + echo "Pest installed!" + echo "Run tests with: php artisan test" + echo "Or: ./vendor/bin/pest" +fi + +# ============================================ +# FILAMENT ADMIN PANEL +# ============================================ +echo "" +echo "============================================" +echo "ADMIN PANEL (Filament)" +echo "============================================" +echo "" +echo "Filament provides a full-featured admin panel with:" +echo " - User management (list, create, edit, delete)" +echo " - Role & permission management" +echo " - Dashboard widgets" +echo " - Form & table builders" +echo "" +read -p "Install Filament Admin Panel? [Y/n]: " INSTALL_FILAMENT + +if [[ ! "$INSTALL_FILAMENT" =~ ^[Nn]$ ]]; then + echo "" + echo "Installing Filament..." + composer require filament/filament:"^3.2" -W + + php artisan filament:install --panels + + echo "" + echo "Creating admin user..." + php artisan make:filament-user + + # Create UserResource for user management + echo "" + echo "Creating User management resource..." + php artisan make:filament-resource User --generate + + echo "" + echo "Filament installed successfully!" + echo "Admin panel available at: /admin" + + # Install Site Settings + echo "" + echo "============================================" + echo "SITE SETTINGS (Appearance)" + echo "============================================" + echo "" + echo "Site settings allow you to manage:" + echo " - Logo and favicon" + echo " - Color scheme" + echo " - Site name" + echo "" + read -p "Install site settings? [Y/n]: " INSTALL_SETTINGS + + if [[ ! "$INSTALL_SETTINGS" =~ ^[Nn]$ ]]; then + echo "" + echo "Installing spatie/laravel-settings..." + composer require spatie/laravel-settings + composer require filament/spatie-laravel-settings-plugin:"^3.2" + + php artisan vendor:publish --provider="Spatie\LaravelSettings\LaravelSettingsServiceProvider" --tag="migrations" + php artisan migrate + + # Create Settings directory + mkdir -p app/Settings + + # Create SiteSettings class + cat > app/Settings/SiteSettings.php << 'SETTINGS' + database/settings/$(date +%Y_%m_%d_%H%M%S)_create_site_settings.php << 'MIGRATION' +migrator->add('site.site_name', config('app.name', 'Laravel')); + $this->migrator->add('site.logo', null); + $this->migrator->add('site.favicon', null); + $this->migrator->add('site.primary_color', '#3b82f6'); + $this->migrator->add('site.secondary_color', '#64748b'); + $this->migrator->add('site.dark_mode', false); + $this->migrator->add('site.footer_text', null); + } +}; +MIGRATION + + # Run settings migration + php artisan migrate + + # Create Filament Settings Page + mkdir -p app/Filament/Pages + cat > app/Filament/Pages/ManageSiteSettings.php << 'PAGE' +schema([ + Forms\Components\Section::make('General') + ->description('Basic site information') + ->schema([ + Forms\Components\TextInput::make('site_name') + ->label('Site Name') + ->required() + ->maxLength(255), + Forms\Components\FileUpload::make('logo') + ->label('Logo') + ->image() + ->directory('site') + ->visibility('public') + ->imageResizeMode('contain') + ->imageCropAspectRatio('16:9') + ->imageResizeTargetWidth('400') + ->helperText('Recommended: 400x100px or similar aspect ratio'), + Forms\Components\FileUpload::make('favicon') + ->label('Favicon') + ->image() + ->directory('site') + ->visibility('public') + ->imageResizeTargetWidth('32') + ->imageResizeTargetHeight('32') + ->helperText('Will be resized to 32x32px'), + ]), + Forms\Components\Section::make('Appearance') + ->description('Customize the look and feel') + ->schema([ + Forms\Components\ColorPicker::make('primary_color') + ->label('Primary Color') + ->required(), + Forms\Components\ColorPicker::make('secondary_color') + ->label('Secondary Color') + ->required(), + Forms\Components\Toggle::make('dark_mode') + ->label('Enable Dark Mode') + ->helperText('Allow users to switch to dark mode'), + ]), + Forms\Components\Section::make('Footer') + ->schema([ + Forms\Components\Textarea::make('footer_text') + ->label('Footer Text') + ->rows(2) + ->placeholder('© 2024 Your Company. All rights reserved.'), + ]), + ]); + } +} +PAGE + + # Create helper function + cat > app/Helpers/site.php << 'HELPER' +{$key} ?? $default; + } +} + +if (!function_exists('site_logo')) { + function site_logo(): ?string + { + $logo = site_settings('logo'); + return $logo ? asset('storage/' . $logo) : null; + } +} + +if (!function_exists('site_favicon')) { + function site_favicon(): ?string + { + $favicon = site_settings('favicon'); + return $favicon ? asset('storage/' . $favicon) : null; + } +} + +if (!function_exists('site_name')) { + function site_name(): string + { + return site_settings('site_name', config('app.name')); + } +} + +if (!function_exists('primary_color')) { + function primary_color(): string + { + return site_settings('primary_color', '#3b82f6'); + } +} + +if (!function_exists('secondary_color')) { + function secondary_color(): string + { + return site_settings('secondary_color', '#64748b'); + } +} +HELPER + + # Register helper in composer.json autoload + php -r " +\$composer = json_decode(file_get_contents('composer.json'), true); +\$composer['autoload']['files'] = \$composer['autoload']['files'] ?? []; +if (!in_array('app/Helpers/site.php', \$composer['autoload']['files'])) { + \$composer['autoload']['files'][] = 'app/Helpers/site.php'; +} +file_put_contents('composer.json', json_encode(\$composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); +" + composer dump-autoload + + # Create Blade component + mkdir -p app/View/Components + cat > app/View/Components/SiteHead.php << 'COMPONENT' + resources/views/components/site-head.blade.php << 'BLADE' +{{-- Site Head Component - Include in your --}} +{{ $title ? $title . ' - ' . site_name() : site_name() }} + +@if(site_favicon()) + +@endif + + +BLADE + + echo "" + echo "Site settings installed!" + echo "Access at: /admin → Settings → Appearance" + fi + + # Install Audit Trail + echo "" + echo "============================================" + echo "AUDIT TRAIL" + echo "============================================" + echo "" + echo "Audit trail tracks all changes to your data:" + echo " - Who made the change" + echo " - What was changed (old → new values)" + echo " - When it happened" + echo " - Which module/model" + echo "" + read -p "Install audit trail system? [Y/n]: " INSTALL_AUDIT + + if [[ ! "$INSTALL_AUDIT" =~ ^[Nn]$ ]]; then + echo "" + echo "Installing owen-it/laravel-auditing..." + composer require owen-it/laravel-auditing + + php artisan vendor:publish --provider="OwenIt\Auditing\AuditingServiceProvider" --tag="config" + php artisan vendor:publish --provider="OwenIt\Auditing\AuditingServiceProvider" --tag="migrations" + php artisan migrate + + # Install Filament Auditing plugin + echo "" + echo "Installing Filament audit trail UI..." + composer require tapp/filament-auditing:"^3.0" + php artisan vendor:publish --tag="filament-auditing-config" + + # Create base Auditable trait for modules + mkdir -p app/Traits + cat > app/Traits/ModuleAuditable.php << 'TRAIT' +getModuleName(), + ]; + } + + /** + * Attributes to include in the audit. + * Override in your model to customize. + */ + // protected $auditInclude = []; + + /** + * Attributes to exclude from the audit. + * Override in your model to customize. + */ + // protected $auditExclude = []; + + /** + * Audit strategy: 'all', 'include', 'exclude' + * Set via module config or model property. + */ + public function getAuditStrategy(): string + { + $moduleName = $this->getModuleName(); + $configKey = strtolower(preg_replace('/(?getModuleName(); + $configKey = strtolower(preg_replace('/(?createToken('token-name')" +fi + +# ============================================ +# STORAGE LINK +# ============================================ +echo "" +echo "Creating storage symlink..." +php artisan storage:link + +# ============================================ +# BASE MIDDLEWARE +# ============================================ +echo "" +echo "============================================" +echo "ADDITIONAL SETUP" +echo "============================================" + +# Force HTTPS in production +read -p "Add ForceHttps middleware for production? [Y/n]: " ADD_HTTPS +if [[ ! "$ADD_HTTPS" =~ ^[Nn]$ ]]; then + mkdir -p app/Http/Middleware + cat > app/Http/Middleware/ForceHttps.php << 'MIDDLEWARE' +environment('production') && !$request->secure()) { + return redirect()->secure($request->getRequestUri()); + } + + return $next($request); + } +} +MIDDLEWARE + echo "ForceHttps middleware created at app/Http/Middleware/ForceHttps.php" + echo "Register in bootstrap/app.php or routes to enable." +fi + +# Security Headers Middleware +read -p "Add SecurityHeaders middleware? [Y/n]: " ADD_SECURITY +if [[ ! "$ADD_SECURITY" =~ ^[Nn]$ ]]; then + cat > app/Http/Middleware/SecurityHeaders.php << 'MIDDLEWARE' +headers->set('X-Frame-Options', 'SAMEORIGIN'); + $response->headers->set('X-Content-Type-Options', 'nosniff'); + $response->headers->set('X-XSS-Protection', '1; mode=block'); + $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); + $response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); + + if (app()->environment('production')) { + $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + } + + return $response; + } +} +MIDDLEWARE + echo "SecurityHeaders middleware created at app/Http/Middleware/SecurityHeaders.php" +fi + +# ============================================ +# MODULE SYSTEM SETUP +# ============================================ +echo "" +echo "============================================" +echo "MODULE SYSTEM" +echo "============================================" +echo "" +echo "The modular architecture allows you to organize features" +echo "into self-contained modules with their own admin panels." +echo "" +read -p "Install module system (spatie/laravel-permission + make:module command)? [Y/n]: " INSTALL_MODULES + +if [[ ! "$INSTALL_MODULES" =~ ^[Nn]$ ]]; then + echo "" + echo "Installing Spatie Permission for role-based access..." + composer require spatie/laravel-permission + php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" + php artisan migrate + + echo "" + echo "Setting up module system..." + + # Create Modules directory + mkdir -p app/Modules + + # Copy ModuleServiceProvider + if [ -f "../src/app/Providers/ModuleServiceProvider.php.stub" ]; then + cp ../src/app/Providers/ModuleServiceProvider.php.stub app/Providers/ModuleServiceProvider.php + else + # Create inline if stub not found + cat > app/Providers/ModuleServiceProvider.php << 'PROVIDER' +app->register($providerClass); + } + } + } + + public function boot(): void + { + $modulesPath = app_path('Modules'); + if (!File::isDirectory($modulesPath)) return; + + foreach (File::directories($modulesPath) as $modulePath) { + $moduleName = basename($modulePath); + + // Load routes + $webRoutes = "{$modulePath}/Routes/web.php"; + $apiRoutes = "{$modulePath}/Routes/api.php"; + if (File::exists($webRoutes)) $this->loadRoutesFrom($webRoutes); + if (File::exists($apiRoutes)) $this->loadRoutesFrom($apiRoutes); + + // Load views + $viewsPath = "{$modulePath}/Resources/views"; + if (File::isDirectory($viewsPath)) { + $slug = strtolower(preg_replace('/(?loadViewsFrom($viewsPath, $slug); + } + + // Load migrations + $migrationsPath = "{$modulePath}/Database/Migrations"; + if (File::isDirectory($migrationsPath)) { + $this->loadMigrationsFrom($migrationsPath); + } + } + } +} +PROVIDER + fi + + # Copy MakeModule command + mkdir -p app/Console/Commands + if [ -f "../src/app/Console/Commands/MakeModule.php.stub" ]; then + cp ../src/app/Console/Commands/MakeModule.php.stub app/Console/Commands/MakeModule.php + fi + + # Register ModuleServiceProvider in bootstrap/providers.php + if [ -f "bootstrap/providers.php" ]; then + if ! grep -q "ModuleServiceProvider" bootstrap/providers.php; then + sed -i 's/];/ App\\Providers\\ModuleServiceProvider::class,\n];/' bootstrap/providers.php + fi + fi + + # Create base permission seeder + cat > database/seeders/PermissionSeeder.php << 'SEEDER' + 'admin', 'guard_name' => 'web']); + $userRole = Role::firstOrCreate(['name' => 'user', 'guard_name' => 'web']); + + // Load permissions from all modules + $modulesPath = app_path('Modules'); + + if (File::isDirectory($modulesPath)) { + foreach (File::directories($modulesPath) as $modulePath) { + $permissionsFile = "{$modulePath}/Permissions.php"; + + if (File::exists($permissionsFile)) { + $permissions = require $permissionsFile; + + foreach ($permissions as $name => $description) { + Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']); + } + + // Give admin all module permissions + $adminRole->givePermissionTo(array_keys($permissions)); + } + } + } + } +} +SEEDER + + echo "" + echo "Module system installed!" + echo "" + echo "Create modules with: php artisan make:module ModuleName" + echo "Options:" + echo " --model=Product Create a model with Filament resource" + echo " --api Include API routes" + echo " --no-filament Skip Filament integration" +fi + +# ============================================ +# CACHE CONFIGURATION +# ============================================ +echo "" +echo "Clearing and caching configuration..." +php artisan config:clear +php artisan cache:clear +php artisan view:clear +php artisan route:clear + +# ============================================ +# FINAL OUTPUT +# ============================================ +echo "" +echo "==========================================" +echo "Laravel Base Setup Complete!" +echo "==========================================" +echo "" +echo "What was configured:" +echo " ✓ Authentication scaffolding (based on selection)" +echo " ✓ Filament Admin Panel (if selected)" +echo " ✓ Sanctum API authentication" +echo " ✓ Storage symlink" +echo " ✓ Security middleware" +echo " ✓ Module system (if selected)" +echo "" +echo "Next steps:" +echo " 1. Review and customize routes in routes/" +echo " 2. Add middleware to bootstrap/app.php if needed" +echo " 3. Create your first module: php artisan make:module YourModule" +echo " 4. Start building your application!" +echo "" +echo "Useful commands:" +echo " make up - Start development server" +echo " make test - Run tests" +echo " make lint - Fix code style" +echo " make fresh - Reset database with seeders" +echo " php artisan make:module Name - Create a new module" +echo "" diff --git a/scripts/post-install.sh b/scripts/post-install.sh new file mode 100644 index 0000000..c73b2e8 --- /dev/null +++ b/scripts/post-install.sh @@ -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' + + + + + + Server Error + + + +
+

500

+

Something went wrong

+

We're sorry, but something unexpected happened. Our team has been notified and is working on it.

+ Go Home +
+ + +EOF + +# 404 Error Page +cat > resources/views/errors/404.blade.php << 'EOF' + + + + + + Page Not Found + + + +
+

404

+

Page not found

+

The page you're looking for doesn't exist or has been moved.

+ Go Home +
+ + +EOF + +# 503 Maintenance Page +cat > resources/views/errors/503.blade.php << 'EOF' + + + + + + Maintenance Mode + + + +
+
🔧
+

Be Right Back

+

We're doing some maintenance

+

We're making some improvements and will be back shortly. Thanks for your patience!

+
+ + +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 diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100644 index 0000000..daa23f0 --- /dev/null +++ b/scripts/restore.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# Database Restore Script +# Usage: ./scripts/restore.sh +# Example: ./scripts/restore.sh backups/laravel_20240306_120000.sql.gz + +set -e + +if [ -z "$1" ]; then + echo "Usage: ./scripts/restore.sh " + 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'" diff --git a/src/.env.example b/src/.env.example new file mode 100644 index 0000000..c5f17ec --- /dev/null +++ b/src/.env.example @@ -0,0 +1,90 @@ +# Laravel Environment Configuration +# +# This template supports multiple databases. Copy the appropriate file: +# - .env.mysql - MySQL configuration +# - .env.pgsql - PostgreSQL configuration +# - .env.sqlite - SQLite configuration +# +# Example: cp .env.mysql .env + +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 + +# Database: Choose one configuration +# =================================== +# MySQL: +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=laravel +DB_USERNAME=laravel +DB_PASSWORD=secret + +# PostgreSQL (uncomment and comment MySQL above): +# DB_CONNECTION=pgsql +# DB_HOST=pgsql +# DB_PORT=5432 +# DB_DATABASE=laravel +# DB_USERNAME=laravel +# DB_PASSWORD=secret + +# SQLite (uncomment and comment MySQL above): +# DB_CONNECTION=sqlite +# DB_DATABASE=/var/www/html/database/database.sqlite + +SESSION_DRIVER=redis +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= + +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}" + +VITE_APP_NAME="${APP_NAME}" + +# Error Logging - Flare (https://flareapp.io) +# Get your key at: https://flareapp.io +FLARE_KEY= + +# Ignition Settings (Development) +# Themes: light, dark, auto +# Editors: vscode, phpstorm, sublime, atom, textmate +IGNITION_THEME=auto +IGNITION_EDITOR=vscode diff --git a/src/.env.mysql b/src/.env.mysql new file mode 100644 index 0000000..d9e0764 --- /dev/null +++ b/src/.env.mysql @@ -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=redis +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= + +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}" + +VITE_APP_NAME="${APP_NAME}" + +# Error Logging - Flare (https://flareapp.io) +# Get your key at: https://flareapp.io +FLARE_KEY= + +# Ignition Settings (Development) +IGNITION_THEME=auto +IGNITION_EDITOR=vscode diff --git a/src/.env.pgsql b/src/.env.pgsql new file mode 100644 index 0000000..47ae2ca --- /dev/null +++ b/src/.env.pgsql @@ -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=redis +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= + +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}" + +VITE_APP_NAME="${APP_NAME}" + +# Error Logging - Flare (https://flareapp.io) +# Get your key at: https://flareapp.io +FLARE_KEY= + +# Ignition Settings (Development) +IGNITION_THEME=auto +IGNITION_EDITOR=vscode diff --git a/src/.env.sqlite b/src/.env.sqlite new file mode 100644 index 0000000..4405066 --- /dev/null +++ b/src/.env.sqlite @@ -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=redis +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= + +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}" + +VITE_APP_NAME="${APP_NAME}" + +# Error Logging - Flare (https://flareapp.io) +# Get your key at: https://flareapp.io +FLARE_KEY= + +# Ignition Settings (Development) +IGNITION_THEME=auto +IGNITION_EDITOR=vscode diff --git a/src/.gitkeep b/src/.gitkeep new file mode 100644 index 0000000..9fa37bf --- /dev/null +++ b/src/.gitkeep @@ -0,0 +1,2 @@ +# This directory will contain your Laravel application +# Run: docker-compose run --rm app composer create-project laravel/laravel . diff --git a/src/app/Console/Commands/MakeModule.php.stub b/src/app/Console/Commands/MakeModule.php.stub new file mode 100644 index 0000000..6b11111 --- /dev/null +++ b/src/app/Console/Commands/MakeModule.php.stub @@ -0,0 +1,1287 @@ +moduleName = $this->argument('name'); + $this->moduleNameStudly = Str::studly($this->moduleName); + $this->moduleNameSlug = Str::slug(Str::snake($this->moduleName)); + $this->moduleNameSnake = Str::snake($this->moduleName); + $this->modulePath = app_path("Modules/{$this->moduleNameStudly}"); + + if (File::isDirectory($this->modulePath)) { + $this->error("Module {$this->moduleNameStudly} already exists!"); + return Command::FAILURE; + } + + $this->info("Creating module: {$this->moduleNameStudly}"); + + $this->createDirectoryStructure(); + $this->createServiceProvider(); + $this->createConfig(); + $this->createRoutes(); + $this->createPermissions(); + $this->createViews(); + $this->createAssets(); + + if (!$this->option('no-filament')) { + $this->createFilamentResource(); + } + + if ($this->option('model')) { + $this->createModel(); + } + + $this->createTests(); + + $this->info(''); + $this->info("✓ Module {$this->moduleNameStudly} created successfully!"); + $this->info(''); + $this->info('Next steps:'); + $this->info(" 1. Run migrations: php artisan migrate"); + $this->info(" 2. Seed permissions: php artisan db:seed --class=PermissionSeeder"); + $this->info(" 3. Run tests: php artisan test tests/Modules/{$this->moduleNameStudly}"); + $this->info(" 4. Access module at: /{$this->moduleNameSlug}"); + $this->info(" 5. Admin panel at: /admin (see {$this->moduleNameStudly} section)"); + + return Command::SUCCESS; + } + + /** + * Create the module directory structure. + */ + protected function createDirectoryStructure(): void + { + $directories = [ + 'Config', + 'Database/Migrations', + 'Database/Seeders', + 'Filament/Resources', + 'Http/Controllers', + 'Http/Middleware', + 'Http/Requests', + 'Models', + 'Policies', + 'Services', + 'Routes', + 'Resources/views', + 'Resources/views/layouts', + 'Resources/views/components', + 'Resources/css', + 'Resources/lang/en', + ]; + + foreach ($directories as $dir) { + File::makeDirectory("{$this->modulePath}/{$dir}", 0755, true); + $this->line(" Created: Modules/{$this->moduleNameStudly}/{$dir}"); + } + } + + /** + * Create the module service provider. + */ + protected function createServiceProvider(): void + { + $content = <<moduleNameStudly}; + +use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Facades\Gate; +use Filament\Facades\Filament; + +class {$this->moduleNameStudly}ServiceProvider extends ServiceProvider +{ + /** + * Register services. + */ + public function register(): void + { + \$this->mergeConfigFrom( + __DIR__.'/Config/{$this->moduleNameSnake}.php', + '{$this->moduleNameSnake}' + ); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + \$this->registerPermissions(); + \$this->registerPolicies(); + } + + /** + * Register module permissions. + */ + protected function registerPermissions(): void + { + \$permissions = require __DIR__.'/Permissions.php'; + + foreach (\$permissions as \$permission => \$description) { + // Permissions are registered via seeder for Spatie Permission + } + } + + /** + * Register module policies. + */ + protected function registerPolicies(): void + { + // Register your policies here + // Gate::policy(Model::class, ModelPolicy::class); + } +} +PHP; + + File::put("{$this->modulePath}/{$this->moduleNameStudly}ServiceProvider.php", $content); + $this->line(" Created: {$this->moduleNameStudly}ServiceProvider.php"); + } + + /** + * Create module config file. + */ + protected function createConfig(): void + { + $content = <<moduleNameStudly} Module Configuration + |-------------------------------------------------------------------------- + */ + + 'name' => '{$this->moduleNameStudly}', + 'slug' => '{$this->moduleNameSlug}', + + // Enable/disable module + 'enabled' => true, + + // Module navigation settings for Filament + 'navigation' => [ + 'group' => '{$this->moduleNameStudly}', + 'icon' => 'heroicon-o-cube', + 'sort' => 10, + ], + + /* + |-------------------------------------------------------------------------- + | Audit Trail Configuration + |-------------------------------------------------------------------------- + | + | Configure how this module tracks changes to data. + | + | Strategies: + | - 'all': Audit all model attributes (default) + | - 'include': Only audit attributes in $auditInclude + | - 'exclude': Audit all except attributes in $auditExclude + | - 'none': Disable auditing for this module + | + */ + 'audit' => [ + 'enabled' => true, + 'strategy' => 'all', + + // Global excludes for all models in this module + 'exclude' => [ + 'password', + 'remember_token', + ], + ], +]; +PHP; + + File::put("{$this->modulePath}/Config/{$this->moduleNameSnake}.php", $content); + $this->line(" Created: Config/{$this->moduleNameSnake}.php"); + } + + /** + * Create module routes. + */ + protected function createRoutes(): void + { + // Web routes + $webRoutes = <<moduleNameStudly}\Http\Controllers\\{$this->moduleNameStudly}Controller; + +/* +|-------------------------------------------------------------------------- +| {$this->moduleNameStudly} Module Web Routes +|-------------------------------------------------------------------------- +*/ + +Route::prefix('{$this->moduleNameSlug}') + ->name('{$this->moduleNameSlug}.') + ->middleware(['web', 'auth']) + ->group(function () { + Route::get('/', [{$this->moduleNameStudly}Controller::class, 'index'])->name('index'); + }); +PHP; + + File::put("{$this->modulePath}/Routes/web.php", $webRoutes); + $this->line(" Created: Routes/web.php"); + + // API routes (if requested) + if ($this->option('api')) { + $apiRoutes = <<moduleNameStudly}\Http\Controllers\Api\\{$this->moduleNameStudly}ApiController; + +/* +|-------------------------------------------------------------------------- +| {$this->moduleNameStudly} Module API Routes +|-------------------------------------------------------------------------- +*/ + +Route::prefix('api/{$this->moduleNameSlug}') + ->name('api.{$this->moduleNameSlug}.') + ->middleware(['api', 'auth:sanctum']) + ->group(function () { + Route::get('/', [{$this->moduleNameStudly}ApiController::class, 'index'])->name('index'); + }); +PHP; + + File::put("{$this->modulePath}/Routes/api.php", $apiRoutes); + File::makeDirectory("{$this->modulePath}/Http/Controllers/Api", 0755, true); + $this->createApiController(); + $this->line(" Created: Routes/api.php"); + } + + // Create web controller + $this->createController(); + } + + /** + * Create the module controller. + */ + protected function createController(): void + { + $content = <<moduleNameStudly}\Http\Controllers; + +use App\Http\Controllers\Controller; +use Illuminate\Http\Request; +use Illuminate\View\View; + +class {$this->moduleNameStudly}Controller extends Controller +{ + /** + * Display the module landing page. + */ + public function index(Request \$request): View + { + return view('{$this->moduleNameSlug}::index', [ + 'title' => '{$this->moduleNameStudly}', + ]); + } +} +PHP; + + File::put("{$this->modulePath}/Http/Controllers/{$this->moduleNameStudly}Controller.php", $content); + $this->line(" Created: Http/Controllers/{$this->moduleNameStudly}Controller.php"); + } + + /** + * Create the API controller. + */ + protected function createApiController(): void + { + $content = <<moduleNameStudly}\Http\Controllers\Api; + +use App\Http\Controllers\Controller; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; + +class {$this->moduleNameStudly}ApiController extends Controller +{ + /** + * Display a listing of the resource. + */ + public function index(Request \$request): JsonResponse + { + return response()->json([ + 'module' => '{$this->moduleNameStudly}', + 'message' => 'API endpoint working', + ]); + } +} +PHP; + + File::put("{$this->modulePath}/Http/Controllers/Api/{$this->moduleNameStudly}ApiController.php", $content); + $this->line(" Created: Http/Controllers/Api/{$this->moduleNameStudly}ApiController.php"); + } + + /** + * Create module permissions. + */ + protected function createPermissions(): void + { + $content = <<moduleNameStudly} Module Permissions +|-------------------------------------------------------------------------- +| +| Define all permissions for this module. These will be registered +| with Spatie Permission and available in Filament Shield. +| +| Format: 'permission_name' => 'Human readable description' +| +*/ + +return [ + '{$this->moduleNameSnake}.view' => 'View {$this->moduleNameStudly}', + '{$this->moduleNameSnake}.create' => 'Create {$this->moduleNameStudly} records', + '{$this->moduleNameSnake}.edit' => 'Edit {$this->moduleNameStudly} records', + '{$this->moduleNameSnake}.delete' => 'Delete {$this->moduleNameStudly} records', + '{$this->moduleNameSnake}.export' => 'Export {$this->moduleNameStudly} data', +]; +PHP; + + File::put("{$this->modulePath}/Permissions.php", $content); + $this->line(" Created: Permissions.php"); + + // Create permission seeder + $seederContent = <<moduleNameStudly}\Database\Seeders; + +use Illuminate\Database\Seeder; +use Spatie\Permission\Models\Permission; +use Spatie\Permission\Models\Role; + +class {$this->moduleNameStudly}PermissionSeeder extends Seeder +{ + /** + * Run the database seeds. + */ + public function run(): void + { + \$permissions = require app_path('Modules/{$this->moduleNameStudly}/Permissions.php'); + + foreach (\$permissions as \$name => \$description) { + Permission::firstOrCreate(['name' => \$name, 'guard_name' => 'web']); + } + + // Assign all module permissions to admin role + \$adminRole = Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']); + \$adminRole->givePermissionTo(array_keys(\$permissions)); + } +} +PHP; + + File::put("{$this->modulePath}/Database/Seeders/{$this->moduleNameStudly}PermissionSeeder.php", $seederContent); + $this->line(" Created: Database/Seeders/{$this->moduleNameStudly}PermissionSeeder.php"); + } + + /** + * Create module views. + */ + protected function createViews(): void + { + // Module layout + $layout = <<<'BLADE' + + + + + + + + {{ $title ?? config('app.name') }} + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + @stack('module-styles') + + +
+ @include('layouts.navigation') + + + @isset($header) +
+
+ {{ $header }} +
+
+ @endisset + + +
+ {{ $slot }} +
+
+ + @stack('module-scripts') + + +BLADE; + + File::put("{$this->modulePath}/Resources/views/layouts/module.blade.php", $layout); + + // Index page + $moduleSlug = $this->moduleNameSlug; + $moduleName = $this->moduleNameStudly; + + $index = << + +

+ {{ __('$moduleName') }} +

+
+ +
+
+
+
+

Welcome to $moduleName

+

+ This is the landing page for the $moduleName module. +

+ + @can('$moduleSlug.view') + + @endcan +
+
+
+
+ + @push('module-styles') + + @endpush + +BLADE; + + File::put("{$this->modulePath}/Resources/views/index.blade.php", $index); + $this->line(" Created: Resources/views/index.blade.php"); + $this->line(" Created: Resources/views/layouts/module.blade.php"); + } + + /** + * Create module assets placeholder. + */ + protected function createAssets(): void + { + $css = <<moduleNameStudly} Module Styles +|-------------------------------------------------------------------------- +| +| Module-specific styles. These are loaded only on module pages. +| Use @push('module-styles') in your Blade templates to include. +| +| For app-wide styles, add to resources/css/app.css instead. +| +*/ + +.{$this->moduleNameSlug}-container { + /* Module container styles */ +} +CSS; + + File::put("{$this->modulePath}/Resources/css/{$this->moduleNameSlug}.css", $css); + $this->line(" Created: Resources/css/{$this->moduleNameSlug}.css"); + } + + /** + * Create Filament resource. + */ + protected function createFilamentResource(): void + { + $content = <<moduleNameStudly}\Filament\Resources; + +use App\Modules\\{$this->moduleNameStudly}\Filament\Resources\\{$this->moduleNameStudly}DashboardResource\Pages; +use Filament\Resources\Resource; +use Filament\Pages\Page; + +class {$this->moduleNameStudly}DashboardResource extends Resource +{ + protected static ?string \$navigationIcon = 'heroicon-o-cube'; + + protected static ?string \$navigationGroup = '{$this->moduleNameStudly}'; + + protected static ?int \$navigationSort = 1; + + protected static ?string \$navigationLabel = 'Dashboard'; + + public static function getPages(): array + { + return [ + 'index' => Pages\{$this->moduleNameStudly}Dashboard::route('/'), + ]; + } + + public static function canAccess(): bool + { + return auth()->user()?->can('{$this->moduleNameSnake}.view') ?? false; + } +} +PHP; + + File::put("{$this->modulePath}/Filament/Resources/{$this->moduleNameStudly}DashboardResource.php", $content); + + // Create pages directory and dashboard page + File::makeDirectory("{$this->modulePath}/Filament/Resources/{$this->moduleNameStudly}DashboardResource/Pages", 0755, true); + + $pagePath = "{$this->modulePath}/Filament/Resources/{$this->moduleNameStudly}DashboardResource/Pages"; + + $pageContent = <<moduleNameStudly}\Filament\Resources\\{$this->moduleNameStudly}DashboardResource\Pages; + +use App\Modules\\{$this->moduleNameStudly}\Filament\Resources\\{$this->moduleNameStudly}DashboardResource; +use Filament\Resources\Pages\Page; +use Filament\Widgets\StatsOverviewWidget\Stat; + +class {$this->moduleNameStudly}Dashboard extends Page +{ + protected static string \$resource = {$this->moduleNameStudly}DashboardResource::class; + + protected static string \$view = '{$this->moduleNameSlug}::filament.dashboard'; + + protected static ?string \$title = '{$this->moduleNameStudly} Dashboard'; + + protected function getHeaderWidgets(): array + { + return [ + // Add widgets here + ]; + } +} +PHP; + + File::put("{$pagePath}/{$this->moduleNameStudly}Dashboard.php", $pageContent); + + // Create Filament view + File::makeDirectory("{$this->modulePath}/Resources/views/filament", 0755, true); + + $dashboardView = << +
+
+

{$this->moduleNameStudly} Module

+

+ Welcome to the {$this->moduleNameStudly} admin dashboard. + Use the sidebar to manage module resources. +

+
+ +
+
+

Quick Stats

+

0

+

Total Records

+
+
+
+ +BLADE; + + File::put("{$this->modulePath}/Resources/views/filament/dashboard.blade.php", $dashboardView); + + $this->line(" Created: Filament/Resources/{$this->moduleNameStudly}DashboardResource.php"); + $this->line(" Created: Filament dashboard page and view"); + + // Create Audit Log Resource + $this->createAuditLogResource(); + } + + /** + * Create Filament Audit Log resource for the module. + */ + protected function createAuditLogResource(): void + { + $auditResource = <<moduleNameStudly}\Filament\Resources; + +use App\Modules\\{$this->moduleNameStudly}\Filament\Resources\AuditLogResource\Pages; +use Filament\Forms; +use Filament\Forms\Form; +use Filament\Resources\Resource; +use Filament\Tables; +use Filament\Tables\Table; +use OwenIt\Auditing\Models\Audit; + +class AuditLogResource extends Resource +{ + protected static ?string \$model = Audit::class; + + protected static ?string \$navigationIcon = 'heroicon-o-clipboard-document-list'; + + protected static ?string \$navigationGroup = '{$this->moduleNameStudly}'; + + protected static ?int \$navigationSort = 99; + + protected static ?string \$navigationLabel = 'Audit Log'; + + protected static ?string \$modelLabel = 'Audit Entry'; + + protected static ?string \$pluralModelLabel = 'Audit Log'; + + public static function table(Table \$table): Table + { + return \$table + ->query( + Audit::query() + ->where('tags', 'like', '%module:{$this->moduleNameStudly}%') + ->latest() + ) + ->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(), + Tables\Columns\TextColumn::make('event') + ->badge() + ->color(fn (string \$state): string => match (\$state) { + 'created' => 'success', + 'updated' => 'warning', + 'deleted' => 'danger', + default => 'gray', + }), + Tables\Columns\TextColumn::make('auditable_type') + ->label('Model') + ->formatStateUsing(fn (string \$state): string => class_basename(\$state)) + ->searchable(), + Tables\Columns\TextColumn::make('auditable_id') + ->label('Record ID'), + Tables\Columns\TextColumn::make('old_values') + ->label('Old Values') + ->formatStateUsing(function (\$state) { + if (empty(\$state)) return '-'; + \$values = is_array(\$state) ? \$state : json_decode(\$state, true); + return collect(\$values)->map(fn(\$v, \$k) => "\$k: \$v")->implode(', '); + }) + ->wrap() + ->limit(50), + Tables\Columns\TextColumn::make('new_values') + ->label('New Values') + ->formatStateUsing(function (\$state) { + if (empty(\$state)) return '-'; + \$values = is_array(\$state) ? \$state : json_decode(\$state, true); + return collect(\$values)->map(fn(\$v, \$k) => "\$k: \$v")->implode(', '); + }) + ->wrap() + ->limit(50), + Tables\Columns\TextColumn::make('ip_address') + ->label('IP') + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('event') + ->options([ + 'created' => 'Created', + 'updated' => 'Updated', + 'deleted' => 'Deleted', + ]), + Tables\Filters\SelectFilter::make('user') + ->relationship('user', 'name') + ->searchable() + ->preload(), + ]) + ->actions([ + Tables\Actions\ViewAction::make() + ->modalContent(fn (Audit \$record) => view('{$this->moduleNameSlug}::filament.audit-detail', ['audit' => \$record])), + ]) + ->defaultSort('created_at', 'desc'); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListAuditLogs::route('/'), + ]; + } + + 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 canAccess(): bool + { + return auth()->user()?->can('{$this->moduleNameSnake}.view') ?? false; + } +} +PHP; + + File::put("{$this->modulePath}/Filament/Resources/AuditLogResource.php", $auditResource); + + // Create pages directory + File::makeDirectory("{$this->modulePath}/Filament/Resources/AuditLogResource/Pages", 0755, true); + + $listPage = <<moduleNameStudly}\Filament\Resources\AuditLogResource\Pages; + +use App\Modules\\{$this->moduleNameStudly}\Filament\Resources\AuditLogResource; +use Filament\Resources\Pages\ListRecords; + +class ListAuditLogs extends ListRecords +{ + protected static string \$resource = AuditLogResource::class; + + protected function getHeaderActions(): array + { + return []; + } +} +PHP; + + File::put("{$this->modulePath}/Filament/Resources/AuditLogResource/Pages/ListAuditLogs.php", $listPage); + + // Create audit detail view + $auditDetailView = <<<'BLADE' +
+
+
+

Event

+

{{ ucfirst($audit->event) }}

+
+
+

Date/Time

+

{{ $audit->created_at->format('M j, Y H:i:s') }}

+
+
+

User

+

{{ $audit->user?->name ?? 'System' }}

+
+
+

IP Address

+

{{ $audit->ip_address ?? 'N/A' }}

+
+
+

Model

+

{{ class_basename($audit->auditable_type) }} #{{ $audit->auditable_id }}

+
+
+

User Agent

+

{{ $audit->user_agent ?? 'N/A' }}

+
+
+ + @if($audit->event === 'updated') +
+

Changes

+ + + + + + + + + + @foreach($audit->new_values as $key => $newValue) + + + + + + @endforeach + +
FieldOld ValueNew Value
{{ $key }}{{ $audit->old_values[$key] ?? '-' }}{{ $newValue }}
+
+ @elseif($audit->event === 'created') +
+

Created Values

+
+ @foreach($audit->new_values as $key => $value) +

{{ $key }}: {{ $value }}

+ @endforeach +
+
+ @elseif($audit->event === 'deleted') +
+

Deleted Values

+
+ @foreach($audit->old_values as $key => $value) +

{{ $key }}: {{ $value }}

+ @endforeach +
+
+ @endif +
+BLADE; + + File::put("{$this->modulePath}/Resources/views/filament/audit-detail.blade.php", $auditDetailView); + + $this->line(" Created: Filament/Resources/AuditLogResource.php"); + $this->line(" Created: Audit log page and detail view"); + } + + /** + * Create a model with migration and Filament resource. + */ + protected function createModel(): void + { + $modelName = Str::studly($this->option('model')); + $tableName = Str::snake(Str::pluralStudly($modelName)); + + // Create Model with Audit Trail + $modelContent = <<moduleNameStudly}\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; + use ModuleAuditable; + + protected \$table = '{$tableName}'; + + protected \$fillable = [ + 'name', + // Add more fields + ]; + + protected \$casts = [ + // Add casts + ]; + + /** + * Attributes to exclude from audit. + * Sensitive fields should be listed here. + */ + protected \$auditExclude = [ + // 'password', + ]; + + /** + * Attributes to include in audit (if using 'include' strategy). + * Leave empty to audit all non-excluded fields. + */ + // protected \$auditInclude = []; +} +PHP; + + File::put("{$this->modulePath}/Models/{$modelName}.php", $modelContent); + $this->line(" Created: Models/{$modelName}.php"); + + // Create Migration + $timestamp = date('Y_m_d_His'); + $migrationContent = <<id(); + \$table->string('name'); + // Add more columns + \$table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('{$tableName}'); + } +}; +PHP; + + File::put("{$this->modulePath}/Database/Migrations/{$timestamp}_create_{$tableName}_table.php", $migrationContent); + $this->line(" Created: Database/Migrations/create_{$tableName}_table.php"); + + // Create Filament Resource for Model + if (!$this->option('no-filament')) { + $this->createModelFilamentResource($modelName, $tableName); + } + } + + /** + * Create Filament resource for a model. + */ + protected function createModelFilamentResource(string $modelName, string $tableName): void + { + $resourceContent = <<moduleNameStudly}\Filament\Resources; + +use App\Modules\\{$this->moduleNameStudly}\Filament\Resources\\{$modelName}Resource\Pages; +use App\Modules\\{$this->moduleNameStudly}\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 = '{$this->moduleNameStudly}'; + + protected static ?int \$navigationSort = 2; + + public static function form(Form \$form): Form + { + return \$form + ->schema([ + Forms\Components\TextInput::make('name') + ->required() + ->maxLength(255), + ]); + } + + public static function table(Table \$table): Table + { + return \$table + ->columns([ + Tables\Columns\TextColumn::make('name') + ->searchable() + ->sortable(), + Tables\Columns\TextColumn::make('created_at') + ->dateTime() + ->sortable(), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]) + ->bulkActions([ + Tables\Actions\BulkActionGroup::make([ + Tables\Actions\DeleteBulkAction::make(), + ]), + ]); + } + + 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'), + ]; + } + + public static function canAccess(): bool + { + return auth()->user()?->can('{$this->moduleNameSnake}.view') ?? false; + } +} +PHP; + + File::put("{$this->modulePath}/Filament/Resources/{$modelName}Resource.php", $resourceContent); + + // Create resource pages + File::makeDirectory("{$this->modulePath}/Filament/Resources/{$modelName}Resource/Pages", 0755, true); + $pagesPath = "{$this->modulePath}/Filament/Resources/{$modelName}Resource/Pages"; + + // List page + $listPage = <<moduleNameStudly}\Filament\Resources\\{$modelName}Resource\Pages; + +use App\Modules\\{$this->moduleNameStudly}\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(), + ]; + } +} +PHP; + File::put("{$pagesPath}/List{$modelName}s.php", $listPage); + + // Create page + $createPage = <<moduleNameStudly}\Filament\Resources\\{$modelName}Resource\Pages; + +use App\Modules\\{$this->moduleNameStudly}\Filament\Resources\\{$modelName}Resource; +use Filament\Resources\Pages\CreateRecord; + +class Create{$modelName} extends CreateRecord +{ + protected static string \$resource = {$modelName}Resource::class; +} +PHP; + File::put("{$pagesPath}/Create{$modelName}.php", $createPage); + + // Edit page + $editPage = <<moduleNameStudly}\Filament\Resources\\{$modelName}Resource\Pages; + +use App\Modules\\{$this->moduleNameStudly}\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(), + ]; + } +} +PHP; + File::put("{$pagesPath}/Edit{$modelName}.php", $editPage); + + $this->line(" Created: Filament/Resources/{$modelName}Resource.php"); + $this->line(" Created: Filament resource pages"); + } + + /** + * Create module tests. + */ + protected function createTests(): void + { + $testPath = base_path("tests/Modules/{$this->moduleNameStudly}"); + File::makeDirectory($testPath, 0755, true, true); + + // Feature test for module routes + $featureTest = <<user = User::factory()->create(); +}); + +describe('{$this->moduleNameStudly} Module', function () { + + it('allows authenticated users to view module page', function () { + \$this->actingAs(\$this->user) + ->get('/{$this->moduleNameSlug}') + ->assertStatus(200); + }); + + it('redirects guests to login', function () { + \$this->get('/{$this->moduleNameSlug}') + ->assertRedirect('/login'); + }); + +}); + +describe('{$this->moduleNameStudly} Admin', function () { + + it('shows module in admin navigation for authorized users', function () { + \$this->user->givePermissionTo('{$this->moduleNameSnake}.view'); + + \$this->actingAs(\$this->user) + ->get('/admin') + ->assertStatus(200); + }); + +}); +PHP; + + File::put("{$testPath}/{$this->moduleNameStudly}Test.php", $featureTest); + + // If model was created, add model tests + if ($this->option('model')) { + $modelName = Str::studly($this->option('model')); + $modelSlug = Str::snake($modelName); + + $modelTest = <<moduleNameStudly}\Models\\{$modelName}; + +beforeEach(function () { + \$this->user = User::factory()->create(); + \$this->user->givePermissionTo([ + '{$this->moduleNameSnake}.view', + '{$this->moduleNameSnake}.create', + '{$this->moduleNameSnake}.edit', + '{$this->moduleNameSnake}.delete', + ]); +}); + +describe('{$modelName} Model', function () { + + it('can be created', function () { + \$model = {$modelName}::create([ + 'name' => 'Test {$modelName}', + ]); + + expect(\$model)->toBeInstanceOf({$modelName}::class); + expect(\$model->name)->toBe('Test {$modelName}'); + }); + + it('is audited on create', function () { + \$this->actingAs(\$this->user); + + \$model = {$modelName}::create([ + 'name' => 'Audited {$modelName}', + ]); + + \$this->assertDatabaseHas('audits', [ + 'auditable_type' => {$modelName}::class, + 'auditable_id' => \$model->id, + 'event' => 'created', + ]); + }); + + it('is audited on update', function () { + \$this->actingAs(\$this->user); + + \$model = {$modelName}::create(['name' => 'Original']); + \$model->update(['name' => 'Updated']); + + \$this->assertDatabaseHas('audits', [ + 'auditable_type' => {$modelName}::class, + 'auditable_id' => \$model->id, + 'event' => 'updated', + ]); + }); + +}); + +describe('{$modelName} API', function () { + + it('can list {$modelSlug}s via API', function () { + {$modelName}::factory()->count(3)->create(); + + \$this->actingAs(\$this->user) + ->getJson('/api/{$this->moduleNameSlug}/{$modelSlug}s') + ->assertStatus(200) + ->assertJsonCount(3, 'data'); + })->skip(fn () => !file_exists(app_path("Modules/{$this->moduleNameStudly}/Routes/api.php")), 'API routes not created'); + +}); +PHP; + + File::put("{$testPath}/{$modelName}Test.php", $modelTest); + $this->line(" Created: tests/Modules/{$this->moduleNameStudly}/{$modelName}Test.php"); + } + + $this->line(" Created: tests/Modules/{$this->moduleNameStudly}/{$this->moduleNameStudly}Test.php"); + } +} diff --git a/src/app/Providers/ModuleServiceProvider.php.stub b/src/app/Providers/ModuleServiceProvider.php.stub new file mode 100644 index 0000000..089f465 --- /dev/null +++ b/src/app/Providers/ModuleServiceProvider.php.stub @@ -0,0 +1,121 @@ +registerModules(); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + $this->bootModules(); + } + + /** + * Register all modules. + */ + protected function registerModules(): void + { + $modulesPath = app_path('Modules'); + + if (!File::isDirectory($modulesPath)) { + return; + } + + $modules = File::directories($modulesPath); + + foreach ($modules as $modulePath) { + $moduleName = basename($modulePath); + $providerClass = "App\\Modules\\{$moduleName}\\{$moduleName}ServiceProvider"; + + if (class_exists($providerClass)) { + $this->app->register($providerClass); + } + } + } + + /** + * Boot all modules. + */ + protected function bootModules(): void + { + $modulesPath = app_path('Modules'); + + if (!File::isDirectory($modulesPath)) { + return; + } + + $modules = File::directories($modulesPath); + + foreach ($modules as $modulePath) { + $moduleName = basename($modulePath); + + // Load module config + $configPath = "{$modulePath}/Config"; + if (File::isDirectory($configPath)) { + foreach (File::files($configPath) as $file) { + $this->mergeConfigFrom( + $file->getPathname(), + strtolower($moduleName) . '.' . $file->getFilenameWithoutExtension() + ); + } + } + + // Load module routes + $this->loadModuleRoutes($modulePath, $moduleName); + + // Load module views + $viewsPath = "{$modulePath}/Resources/views"; + if (File::isDirectory($viewsPath)) { + $this->loadViewsFrom($viewsPath, strtolower(preg_replace('/(?loadMigrationsFrom($migrationsPath); + } + + // Load module translations + $langPath = "{$modulePath}/Resources/lang"; + if (File::isDirectory($langPath)) { + $this->loadTranslationsFrom($langPath, strtolower($moduleName)); + } + } + } + + /** + * Load module routes. + */ + protected function loadModuleRoutes(string $modulePath, string $moduleName): void + { + $routesPath = "{$modulePath}/Routes"; + + if (!File::isDirectory($routesPath)) { + return; + } + + $webRoutes = "{$routesPath}/web.php"; + $apiRoutes = "{$routesPath}/api.php"; + + if (File::exists($webRoutes)) { + $this->loadRoutesFrom($webRoutes); + } + + if (File::exists($apiRoutes)) { + $this->loadRoutesFrom($apiRoutes); + } + } +} diff --git a/src/config/cors.php.example b/src/config/cors.php.example new file mode 100644 index 0000000..a1b8bb2 --- /dev/null +++ b/src/config/cors.php.example @@ -0,0 +1,41 @@ + ['api/*', 'sanctum/csrf-cookie'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => [ + env('FRONTEND_URL', 'http://localhost:3000'), + // Add other allowed origins here + ], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => true, + +]; diff --git a/src/pint.json b/src/pint.json new file mode 100644 index 0000000..cc32862 --- /dev/null +++ b/src/pint.json @@ -0,0 +1,45 @@ +{ + "preset": "laravel", + "rules": { + "array_indentation": true, + "array_syntax": { + "syntax": "short" + }, + "binary_operator_spaces": { + "default": "single_space" + }, + "blank_line_after_namespace": true, + "blank_line_after_opening_tag": true, + "blank_line_before_statement": { + "statements": ["return", "throw", "try"] + }, + "class_attributes_separation": { + "elements": { + "method": "one" + } + }, + "concat_space": { + "spacing": "none" + }, + "declare_strict_types": false, + "fully_qualified_strict_types": true, + "method_argument_space": { + "on_multiline": "ensure_fully_multiline", + "keep_multiple_spaces_after_comma": false + }, + "no_unused_imports": true, + "ordered_imports": { + "sort_algorithm": "alpha" + }, + "single_quote": true, + "trailing_comma_in_multiline": { + "elements": ["arrays", "arguments", "parameters"] + } + }, + "exclude": [ + "vendor", + "node_modules", + "storage", + "bootstrap/cache" + ] +} diff --git a/src/routes/api.example.php b/src/routes/api.example.php new file mode 100644 index 0000000..c2a107c --- /dev/null +++ b/src/routes/api.example.php @@ -0,0 +1,94 @@ +json([ + 'status' => 'ok', + 'timestamp' => now()->toISOString(), + ]); +}); + +/* +|-------------------------------------------------------------------------- +| Authentication Routes (if using Breeze API or custom) +|-------------------------------------------------------------------------- +*/ + +// Route::post('/register', [AuthController::class, 'register']); +// Route::post('/login', [AuthController::class, 'login']); +// Route::post('/forgot-password', [AuthController::class, 'forgotPassword']); +// Route::post('/reset-password', [AuthController::class, 'resetPassword']); + +/* +|-------------------------------------------------------------------------- +| Protected API Routes (require authentication) +|-------------------------------------------------------------------------- +*/ + +Route::middleware('auth:sanctum')->group(function () { + + // Current user + Route::get('/user', function (Request $request) { + return $request->user(); + }); + + // Logout + Route::post('/logout', function (Request $request) { + $request->user()->currentAccessToken()->delete(); + return response()->json(['message' => 'Logged out']); + }); + + // User tokens management + Route::get('/tokens', function (Request $request) { + return $request->user()->tokens; + }); + + Route::delete('/tokens/{tokenId}', function (Request $request, $tokenId) { + $request->user()->tokens()->where('id', $tokenId)->delete(); + return response()->json(['message' => 'Token revoked']); + }); + + /* + |-------------------------------------------------------------------------- + | Your API Resources + |-------------------------------------------------------------------------- + | Add your protected API routes here + | + | Example: + | Route::apiResource('posts', PostController::class); + | Route::apiResource('comments', CommentController::class); + | + */ + +}); + +/* +|-------------------------------------------------------------------------- +| API Versioning Example +|-------------------------------------------------------------------------- +| Uncomment to use versioned API routes +| +| Route::prefix('v1')->group(function () { +| Route::apiResource('posts', Api\V1\PostController::class); +| }); +| +| Route::prefix('v2')->group(function () { +| Route::apiResource('posts', Api\V2\PostController::class); +| }); +| +*/