Merge pull request 'Feature/core system' (#2) from feature/core-system into develop

Reviewed-on: http://git.vm:3000/Admin/WMS-Core/pulls/2
This commit is contained in:
2026-06-10 14:27:23 +01:00
5099 changed files with 281078 additions and 236 deletions
+22
View File
@@ -0,0 +1,22 @@
# WMS Fix Notes
This build patches only the current uploaded code.
## Fixed
- Duplicate two-factor migration made safe.
- Primary two-factor migration adds columns one at a time for SQLite safety.
- Module page slot is wrapped in a visible `.wms-content` main container.
- Sidebar remains traditional: desktop/tablet icon rail, mobile drawer.
- Sidebar widened for "Warehouse Management System".
- Laravel cache folders are included so "Please provide a valid cache path" does not recur after unzip.
## After unzip
```bash
composer install
npm install
php artisan optimize:clear
php artisan test
npm run build
```
@@ -0,0 +1,19 @@
<?php
namespace Modules\InternalApi\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Modules\InternalApi\app\Http\Middleware\VerifyInternalApiSignature;
class InternalApiServiceProvider extends ServiceProvider
{
/**
* Bootstrap module services.
*/
public function boot(): void
{
Route::middleware('api')->prefix('api/internal')->group(__DIR__ . '/../routes/api.php');
app('router')->aliasMiddleware('internal.signed', VerifyInternalApiSignature::class);
}
}
@@ -0,0 +1,19 @@
<?php
namespace Modules\InternalApi\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Modules\Platform\app\Models\Company;
class CompanySummaryController extends Controller
{
/**
* Return tenant summary for authenticated internal callers.
*/
public function __invoke()
{
return response()->json([
'companies' => Company::withCount(['users', 'warehouses', 'licenseKeys'])->orderBy('name')->get(),
]);
}
}
@@ -0,0 +1,39 @@
<?php
namespace Modules\InternalApi\app\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class VerifyInternalApiSignature
{
/**
* Verify the HMAC signature used between separated WMS applications.
*/
public function handle(Request $request, Closure $next): Response
{
$secret = (string) config('platform.internal_api_key');
$timestamp = (string) $request->header('X-WMS-Timestamp');
$signature = (string) $request->header('X-WMS-Signature');
abort_if($secret === '' || $timestamp === '' || $signature === '', 401);
abort_if(strtotime($timestamp) === false, 401, 'Invalid internal API timestamp.');
abort_if(abs(now()->timestamp - strtotime($timestamp)) > 300, 401, 'Expired internal API signature.');
$body = $request->getContent();
$payloads = array_unique([
$timestamp . '.' . $body,
$timestamp . '.',
$timestamp . '.[]',
]);
foreach ($payloads as $payload) {
if (hash_equals(hash_hmac('sha256', $payload, $secret), $signature)) {
return $next($request);
}
}
abort(401, 'Invalid internal API signature.');
}
}
+1
View File
@@ -0,0 +1 @@
{"name":"InternalApi","alias":"internal-api","description":"Authenticated internal API between separated WMS applications.","keywords":["api","hmac","integration"],"priority":2,"providers":["Modules\\InternalApi\\Providers\\InternalApiServiceProvider"],"files":[]}
+8
View File
@@ -0,0 +1,8 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\InternalApi\app\Http\Controllers\CompanySummaryController;
Route::middleware('internal.signed')->group(function (): void {
Route::get('/companies', CompanySummaryController::class)->name('internal.companies.index');
});
@@ -0,0 +1,29 @@
<?php
namespace Modules\InternalApi\tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class InternalApiSignatureTest extends TestCase
{
use RefreshDatabase;
public function test_internal_api_rejects_missing_signature(): void
{
$this->getJson('/api/internal/companies')->assertUnauthorized();
}
public function test_internal_api_accepts_valid_signature(): void
{
config(['platform.internal_api_key' => 'testing-secret']);
$timestamp = now()->toIso8601String();
$body = '';
$signature = hash_hmac('sha256', $timestamp . '.' . $body, 'testing-secret');
$this->withHeaders([
'X-WMS-Timestamp' => $timestamp,
'X-WMS-Signature' => $signature,
])->getJson('/api/internal/companies')->assertOk();
}
}
@@ -0,0 +1,36 @@
<?php
namespace Modules\Platform\Providers;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Modules\Platform\app\Http\Middleware\EnsureLicensedTenant;
use Modules\Platform\app\Http\Middleware\EnsurePlatformAdmin;
use Modules\Platform\app\Http\Middleware\EnsureTenantAssigned;
class PlatformServiceProvider extends ServiceProvider
{
/**
* Register module services.
*/
public function register(): void
{
$this->mergeConfigFrom(__DIR__ . '/../config/platform.php', 'platform');
}
/**
* Bootstrap module services.
*/
public function boot(): void
{
$this->loadViewsFrom(__DIR__ . '/../resources/views', 'platform');
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
Route::middleware('web')->group(__DIR__ . '/../routes/web.php');
app('router')->aliasMiddleware('tenant.assigned', EnsureTenantAssigned::class);
app('router')->aliasMiddleware('tenant.licensed', EnsureLicensedTenant::class);
app('router')->aliasMiddleware('platform.admin', EnsurePlatformAdmin::class);
}
}
@@ -0,0 +1,41 @@
<?php
namespace Modules\Platform\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Modules\Platform\app\Models\Company;
class CompanyController extends Controller
{
/**
* Display companies.
*/
public function index()
{
return view('platform::companies.index', [
'companies' => Company::withCount(['users', 'warehouses', 'licenseKeys'])->orderBy('name')->paginate(20),
]);
}
/**
* Create a company tenant.
*/
public function store(Request $request)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'billing_email' => ['nullable', 'email', 'max:255'],
'status' => ['required', 'in:active,suspended'],
]);
Company::create([
...$data,
'slug' => Str::slug($data['name']),
'timezone' => 'Europe/London',
]);
return back()->with('status', 'Company created.');
}
}
@@ -0,0 +1,41 @@
<?php
namespace Modules\Platform\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Platform\app\Models\Company;
use Modules\Platform\app\Models\LicenseKey;
class LicenseKeyController extends Controller
{
/**
* Display licence keys.
*/
public function index()
{
return view('platform::licenses.index', [
'companies' => Company::orderBy('name')->get(),
'licenses' => LicenseKey::with('company')->latest()->paginate(20),
'plainKey' => session('plain_license_key'),
]);
}
/**
* Issue a new licence key.
*/
public function store(Request $request)
{
$data = $request->validate([
'company_id' => ['required', 'exists:companies,id'],
'label' => ['required', 'string', 'max:255'],
'plan' => ['required', 'in:standard,professional,enterprise'],
'max_users' => ['required', 'integer', 'min:1', 'max:10000'],
]);
$company = Company::findOrFail($data['company_id']);
$plainKey = LicenseKey::issueFor($company, $data['label'], $data['plan'], (int) $data['max_users']);
return back()->with('plain_license_key', $plainKey)->with('status', 'Licence key issued. Copy it now.');
}
}
@@ -0,0 +1,44 @@
<?php
namespace Modules\Platform\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Platform\app\Models\Company;
use Modules\Platform\app\Models\Payment;
class PaymentController extends Controller
{
/**
* Display company payments.
*/
public function index()
{
return view('platform::payments.index', [
'companies' => Company::orderBy('name')->get(),
'payments' => Payment::with('company')->latest()->paginate(20),
]);
}
/**
* Add a manual payment or admin override record.
*/
public function store(Request $request)
{
$data = $request->validate([
'company_id' => ['required', 'exists:companies,id'],
'amount' => ['required', 'integer', 'min:0'],
'currency' => ['required', 'string', 'size:3'],
'status' => ['required', 'in:pending,paid,failed,refunded,override'],
'description' => ['nullable', 'string', 'max:255'],
]);
Payment::create([
...$data,
'paid_at' => in_array($data['status'], ['paid', 'override'], true) ? now() : null,
'metadata' => ['source' => 'manual_admin'],
]);
return back()->with('status', 'Payment record created.');
}
}
@@ -0,0 +1,27 @@
<?php
namespace Modules\Platform\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Modules\Platform\app\Models\Company;
use Modules\Platform\app\Models\LicenseKey;
use Modules\Platform\app\Models\Payment;
use Modules\Warehouse\app\Models\Warehouse;
class PlatformDashboardController extends Controller
{
/**
* Show the platform administration dashboard.
*/
public function __invoke()
{
return view('platform::dashboard.index', [
'companiesCount' => Company::count(),
'activeCompaniesCount' => Company::where('status', 'active')->count(),
'licensesCount' => LicenseKey::count(),
'paymentsTotal' => Payment::where('status', 'paid')->sum('amount'),
'warehousesCount' => Warehouse::count(),
'companies' => Company::withCount(['users', 'warehouses', 'licenseKeys'])->latest()->limit(8)->get(),
]);
}
}
@@ -0,0 +1,42 @@
<?php
namespace Modules\Platform\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Platform\app\Models\Company;
use Modules\Platform\app\Models\Payment;
class StripeWebhookController extends Controller
{
/**
* Receive a Stripe webhook event.
*/
public function __invoke(Request $request)
{
$payload = $request->all();
$eventType = data_get($payload, 'type');
if ($eventType === 'payment_intent.succeeded') {
$intent = data_get($payload, 'data.object', []);
$company = Company::where('stripe_customer_id', data_get($intent, 'customer'))->first();
if ($company) {
Payment::updateOrCreate(
['stripe_payment_intent_id' => data_get($intent, 'id')],
[
'company_id' => $company->id,
'amount' => (int) data_get($intent, 'amount_received', 0),
'currency' => (string) data_get($intent, 'currency', 'gbp'),
'status' => 'paid',
'description' => 'Stripe payment received',
'paid_at' => now(),
'metadata' => $intent,
]
);
}
}
return response()->json(['received' => true]);
}
}
@@ -0,0 +1,29 @@
<?php
namespace Modules\Platform\app\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureLicensedTenant
{
/**
* Ensure the current company has an active licence unless it is the MPBA platform tenant.
*/
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
$company = $user?->company;
abort_if(! $company, 403, 'No company is assigned to this account.');
if ($user->isMpbaPlatformAdmin()) {
return $next($request);
}
abort_unless($company->hasActiveLicense(), 402, 'Payment Required');
return $next($request);
}
}
@@ -0,0 +1,20 @@
<?php
namespace Modules\Platform\app\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsurePlatformAdmin
{
/**
* Ensure the current user is an MPBA platform administrator.
*/
public function handle(Request $request, Closure $next): Response
{
abort_unless($request->user()?->isMpbaPlatformAdmin(), 403);
return $next($request);
}
}
@@ -0,0 +1,22 @@
<?php
namespace Modules\Platform\app\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureTenantAssigned
{
/**
* Ensure the authenticated user belongs to a company tenant.
*/
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
abort_if(! $user || ! $user->company_id, 403, 'No company is assigned to this account.');
return $next($request);
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
namespace Modules\Platform\app\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
use Modules\Warehouse\app\Models\Warehouse;
class Company extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'uuid', 'name', 'slug', 'status', 'timezone', 'settings',
'is_system_company', 'stripe_customer_id', 'billing_email',
];
protected $casts = [
'settings' => 'array',
'is_system_company' => 'boolean',
];
protected static function booted(): void
{
static::creating(function (Company $company): void {
$company->uuid ??= (string) Str::uuid();
$company->slug ??= Str::slug($company->name);
$company->status ??= 'active';
$company->timezone ??= 'Europe/London';
});
}
public function users(): HasMany
{
return $this->hasMany(\App\Models\User::class);
}
public function licenseKeys(): HasMany
{
return $this->hasMany(LicenseKey::class);
}
public function payments(): HasMany
{
return $this->hasMany(Payment::class);
}
public function warehouses(): HasMany
{
return $this->hasMany(Warehouse::class);
}
public function hasActiveLicense(): bool
{
if ($this->is_system_company) {
return true;
}
return $this->licenseKeys()
->where('is_active', true)
->where(function ($query): void {
$query->whereNull('starts_at')->orWhere('starts_at', '<=', now());
})
->where(function ($query): void {
$query->whereNull('expires_at')->orWhere('expires_at', '>=', now());
})
->whereNull('revoked_at')
->exists();
}
}
@@ -0,0 +1,47 @@
<?php
namespace Modules\Platform\app\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class LicenseKey extends Model
{
protected $fillable = [
'company_id', 'key_hash', 'key_prefix', 'label', 'plan', 'max_users',
'is_active', 'starts_at', 'expires_at', 'last_used_at', 'revoked_at',
];
protected $casts = [
'is_active' => 'boolean',
'starts_at' => 'datetime',
'expires_at' => 'datetime',
'last_used_at' => 'datetime',
'revoked_at' => 'datetime',
];
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
public static function issueFor(Company $company, string $label, string $plan = 'standard', int $maxUsers = 10): string
{
$plainKey = 'WMS-' . Str::upper(Str::random(32));
self::create([
'company_id' => $company->id,
'key_hash' => hash('sha256', $plainKey),
'key_prefix' => Str::substr($plainKey, 0, 12),
'label' => $label,
'plan' => $plan,
'max_users' => $maxUsers,
'is_active' => true,
'starts_at' => now(),
'expires_at' => now()->addYear(),
]);
return $plainKey;
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace Modules\Platform\app\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Payment extends Model
{
protected $fillable = [
'company_id', 'stripe_payment_intent_id', 'stripe_invoice_id', 'amount',
'currency', 'status', 'description', 'paid_at', 'metadata',
];
protected $casts = [
'amount' => 'integer',
'paid_at' => 'datetime',
'metadata' => 'array',
];
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
return [
'system_company_slug' => env('WMS_SYSTEM_COMPANY_SLUG', 'mpba-limited'),
'system_admin_email' => env('WMS_SYSTEM_ADMIN_EMAIL', 'mark.gregory@mpba.co.uk'),
'internal_api_key' => env('WMS_INTERNAL_API_KEY', 'local-dev-change-me'),
'stripe' => [
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],
];
@@ -0,0 +1,80 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('companies', function (Blueprint $table): void {
$table->id();
$table->uuid('uuid')->unique();
$table->string('name');
$table->string('slug')->unique();
$table->string('status')->default('active')->index();
$table->string('timezone')->default('Europe/London');
$table->boolean('is_system_company')->default(false)->index();
$table->string('stripe_customer_id')->nullable()->index();
$table->string('billing_email')->nullable();
$table->json('settings')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::table('users', function (Blueprint $table): void {
$table->foreignId('company_id')->nullable()->after('id')->constrained('companies')->nullOnDelete();
$table->boolean('is_platform_admin')->default(false)->after('password');
$table->boolean('is_company_admin')->default(false)->after('is_platform_admin');
$table->string('status')->default('active')->after('is_company_admin')->index();
});
Schema::create('license_keys', function (Blueprint $table): void {
$table->id();
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
$table->string('key_hash', 64)->unique();
$table->string('key_prefix', 16)->index();
$table->string('label');
$table->string('plan')->default('standard')->index();
$table->unsignedInteger('max_users')->default(10);
$table->boolean('is_active')->default(true)->index();
$table->timestamp('starts_at')->nullable();
$table->timestamp('expires_at')->nullable()->index();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('revoked_at')->nullable()->index();
$table->timestamps();
});
Schema::create('payments', function (Blueprint $table): void {
$table->id();
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
$table->string('stripe_payment_intent_id')->nullable()->unique();
$table->string('stripe_invoice_id')->nullable()->index();
$table->unsignedInteger('amount')->default(0);
$table->string('currency', 3)->default('gbp');
$table->string('status')->default('pending')->index();
$table->string('description')->nullable();
$table->timestamp('paid_at')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('payments');
Schema::dropIfExists('license_keys');
Schema::table('users', function (Blueprint $table): void {
$table->dropConstrainedForeignId('company_id');
$table->dropColumn(['is_platform_admin', 'is_company_admin', 'status']);
});
Schema::dropIfExists('companies');
}
};
@@ -0,0 +1,55 @@
<?php
namespace Modules\Platform\database\seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Modules\Platform\app\Models\Company;
use Modules\Warehouse\app\Models\Warehouse;
use Modules\Warehouse\app\Models\WarehouseValidationRule;
class PlatformSeeder extends Seeder
{
/**
* Seed the MPBA system tenant and administrator.
*/
public function run(): void
{
$company = Company::updateOrCreate(
['slug' => 'mpba-limited'],
[
'uuid' => (string) Str::uuid(),
'name' => 'MPBA Limited',
'status' => 'active',
'timezone' => 'Europe/London',
'is_system_company' => true,
'billing_email' => 'mark.gregory@mpba.co.uk',
]
);
User::updateOrCreate(
['email' => 'mark.gregory@mpba.co.uk'],
[
'company_id' => $company->id,
'name' => 'Mark Gregory',
'password' => Hash::make('987400Kmh'),
'is_platform_admin' => true,
'is_company_admin' => true,
'status' => 'active',
'email_verified_at' => now(),
]
);
$warehouse = Warehouse::updateOrCreate(
['company_id' => $company->id, 'code' => 'MPBA-MAIN'],
['name' => 'MPBA Main Warehouse', 'status' => 'active', 'timezone' => 'Europe/London']
);
WarehouseValidationRule::updateOrCreate(
['warehouse_id' => $warehouse->id, 'rule_key' => 'stock.location.required'],
['name' => 'Stock location required', 'severity' => 'error', 'is_active' => true, 'configuration' => ['required' => true]]
);
}
}
+1
View File
@@ -0,0 +1 @@
{"name":"Platform","alias":"platform","description":"System administration, tenants, licences, Stripe payments and tenant guards.","keywords":["wms","multi-tenant","licensing"],"priority":0,"providers":["Modules\\Platform\\Providers\\PlatformServiceProvider"],"files":[]}
@@ -0,0 +1,21 @@
<x-layouts::app :title="__('Companies')">
<div class="space-y-6">
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<h1 class="text-2xl font-bold">Companies</h1>
<form method="POST" action="{{ route('platform.companies.store') }}" class="mt-5 grid gap-4 md:grid-cols-4">
@csrf
<input name="name" placeholder="Company name" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950" required>
<input name="billing_email" placeholder="billing@example.com" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950">
<select name="status" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950"><option value="active">Active</option><option value="suspended">Suspended</option></select>
<button class="rounded-xl bg-zinc-900 px-4 py-2 font-semibold text-white dark:bg-white dark:text-zinc-900">Create</button>
</form>
</div>
<div class="overflow-x-auto rounded-2xl bg-white shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<table class="min-w-full text-sm">
<thead><tr class="border-b"><th class="p-4 text-left">Name</th><th class="p-4 text-left">Users</th><th class="p-4 text-left">Warehouses</th><th class="p-4 text-left">Licences</th><th class="p-4 text-left">Status</th></tr></thead>
<tbody>@foreach($companies as $company)<tr class="border-b border-zinc-100 dark:border-zinc-800"><td class="p-4 font-semibold">{{ $company->name }}</td><td class="p-4">{{ $company->users_count }}</td><td class="p-4">{{ $company->warehouses_count }}</td><td class="p-4">{{ $company->license_keys_count }}</td><td class="p-4">{{ $company->is_system_company ? 'System' : $company->status }}</td></tr>@endforeach</tbody>
</table>
</div>
{{ $companies->links() }}
</div>
</x-layouts::app>
@@ -0,0 +1,60 @@
<x-layouts::app :title="__('Platform Dashboard')">
<div class="space-y-6">
<section class="overflow-hidden rounded-3xl bg-zinc-950 p-6 text-white shadow-xl sm:p-8">
<div class="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div>
<p class="text-sm font-semibold uppercase tracking-[0.3em] text-cyan-300">MPBA WMS</p>
<h1 class="mt-3 text-3xl font-bold tracking-tight sm:text-5xl">Platform administration</h1>
<p class="mt-3 max-w-2xl text-zinc-300">Manage tenant companies, licences, payments, warehouses and internal API operations from one resilient control plane.</p>
</div>
<div class="rounded-2xl bg-white/10 p-5 backdrop-blur">
<p class="text-sm text-zinc-300">System status</p>
<p class="mt-2 text-2xl font-bold text-emerald-300">Operational</p>
</div>
</div>
</section>
<section class="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
@foreach ([['Companies', $companiesCount], ['Active', $activeCompaniesCount], ['Warehouses', $warehousesCount], ['Licences', $licensesCount], ['Paid £', number_format($paymentsTotal / 100, 2)]] as [$label, $value])
<article class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ $label }}</p>
<p class="mt-2 text-3xl font-bold text-zinc-900 dark:text-white">{{ $value }}</p>
</article>
@endforeach
</section>
<section class="rounded-2xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<div class="flex flex-col gap-3 border-b border-zinc-200 p-5 sm:flex-row sm:items-center sm:justify-between dark:border-zinc-800">
<div>
<h2 class="text-lg font-semibold text-zinc-900 dark:text-white">Recent companies</h2>
<p class="text-sm text-zinc-500">Tenant isolation is enforced by company ownership on every warehouse record.</p>
</div>
<a href="{{ route('platform.companies.index') }}" class="rounded-lg bg-zinc-900 px-4 py-2 text-sm font-semibold text-white dark:bg-white dark:text-zinc-900">Manage companies</a>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-zinc-200 text-sm dark:divide-zinc-800">
<thead class="bg-zinc-50 dark:bg-zinc-950">
<tr>
<th class="px-5 py-3 text-left font-semibold">Company</th>
<th class="px-5 py-3 text-left font-semibold">Users</th>
<th class="px-5 py-3 text-left font-semibold">Warehouses</th>
<th class="px-5 py-3 text-left font-semibold">Licences</th>
<th class="px-5 py-3 text-left font-semibold">Status</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-200 dark:divide-zinc-800">
@foreach ($companies as $company)
<tr>
<td class="px-5 py-4 font-medium">{{ $company->name }}</td>
<td class="px-5 py-4">{{ $company->users_count }}</td>
<td class="px-5 py-4">{{ $company->warehouses_count }}</td>
<td class="px-5 py-4">{{ $company->license_keys_count }}</td>
<td class="px-5 py-4">{{ $company->is_system_company ? 'System' : ucfirst($company->status) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</section>
</div>
</x-layouts::app>
@@ -0,0 +1,18 @@
<x-layouts::app :title="__('Licence Keys')">
<div class="space-y-6">
@if($plainKey)<div class="rounded-2xl bg-emerald-50 p-4 text-emerald-900 ring-1 ring-emerald-200">New key: <code class="font-bold">{{ $plainKey }}</code></div>@endif
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<h1 class="text-2xl font-bold">Licence keys</h1>
<form method="POST" action="{{ route('platform.licenses.store') }}" class="mt-5 grid gap-4 md:grid-cols-5">
@csrf
<select name="company_id" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950">@foreach($companies as $company)<option value="{{ $company->id }}">{{ $company->name }}</option>@endforeach</select>
<input name="label" placeholder="Licence label" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950" required>
<select name="plan" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950"><option>standard</option><option>professional</option><option>enterprise</option></select>
<input name="max_users" type="number" value="10" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950" required>
<button class="rounded-xl bg-zinc-900 px-4 py-2 font-semibold text-white dark:bg-white dark:text-zinc-900">Issue</button>
</form>
</div>
<div class="overflow-x-auto rounded-2xl bg-white shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800"><table class="min-w-full text-sm"><thead><tr class="border-b"><th class="p-4 text-left">Company</th><th class="p-4 text-left">Label</th><th class="p-4 text-left">Plan</th><th class="p-4 text-left">Prefix</th><th class="p-4 text-left">Expires</th></tr></thead><tbody>@foreach($licenses as $license)<tr class="border-b border-zinc-100 dark:border-zinc-800"><td class="p-4">{{ $license->company->name }}</td><td class="p-4 font-semibold">{{ $license->label }}</td><td class="p-4">{{ $license->plan }}</td><td class="p-4"><code>{{ $license->key_prefix }}</code></td><td class="p-4">{{ optional($license->expires_at)->toDateString() }}</td></tr>@endforeach</tbody></table></div>
{{ $licenses->links() }}
</div>
</x-layouts::app>
@@ -0,0 +1,7 @@
@props(['title' => 'WMS Platform'])
<div class="min-h-screen bg-zinc-50 dark:bg-zinc-950">
<div class="mx-auto flex max-w-7xl flex-col gap-6 px-4 py-6 sm:px-6 lg:px-8">
{{ $slot }}
</div>
</div>
@@ -0,0 +1,18 @@
<x-layouts::app :title="__('Payments')">
<div class="space-y-6">
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<h1 class="text-2xl font-bold">Payments and overrides</h1>
<form method="POST" action="{{ route('platform.payments.store') }}" class="mt-5 grid gap-4 md:grid-cols-6">
@csrf
<select name="company_id" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950">@foreach($companies as $company)<option value="{{ $company->id }}">{{ $company->name }}</option>@endforeach</select>
<input name="amount" type="number" value="0" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950" required>
<input name="currency" value="gbp" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950" required>
<select name="status" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950"><option>paid</option><option>pending</option><option>failed</option><option>refunded</option><option>override</option></select>
<input name="description" placeholder="Description" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950">
<button class="rounded-xl bg-zinc-900 px-4 py-2 font-semibold text-white dark:bg-white dark:text-zinc-900">Add</button>
</form>
</div>
<div class="overflow-x-auto rounded-2xl bg-white shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800"><table class="min-w-full text-sm"><thead><tr class="border-b"><th class="p-4 text-left">Company</th><th class="p-4 text-left">Amount</th><th class="p-4 text-left">Status</th><th class="p-4 text-left">Description</th></tr></thead><tbody>@foreach($payments as $payment)<tr class="border-b border-zinc-100 dark:border-zinc-800"><td class="p-4">{{ $payment->company->name }}</td><td class="p-4 font-semibold">{{ strtoupper($payment->currency) }} {{ number_format($payment->amount / 100, 2) }}</td><td class="p-4">{{ $payment->status }}</td><td class="p-4">{{ $payment->description }}</td></tr>@endforeach</tbody></table></div>
{{ $payments->links() }}
</div>
</x-layouts::app>
+23
View File
@@ -0,0 +1,23 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Platform\app\Http\Controllers\CompanyController;
use Modules\Platform\app\Http\Controllers\LicenseKeyController;
use Modules\Platform\app\Http\Controllers\PaymentController;
use Modules\Platform\app\Http\Controllers\PlatformDashboardController;
use Modules\Platform\app\Http\Controllers\StripeWebhookController;
Route::middleware(['auth', 'verified', 'tenant.assigned', 'platform.admin'])
->prefix('admin')
->name('platform.')
->group(function (): void {
Route::get('/', PlatformDashboardController::class)->name('dashboard');
Route::get('/companies', [CompanyController::class, 'index'])->name('companies.index');
Route::post('/companies', [CompanyController::class, 'store'])->name('companies.store');
Route::get('/licenses', [LicenseKeyController::class, 'index'])->name('licenses.index');
Route::post('/licenses', [LicenseKeyController::class, 'store'])->name('licenses.store');
Route::get('/payments', [PaymentController::class, 'index'])->name('payments.index');
Route::post('/payments', [PaymentController::class, 'store'])->name('payments.store');
});
Route::post('/stripe/webhook', StripeWebhookController::class)->name('stripe.webhook');
@@ -0,0 +1,26 @@
<?php
namespace Modules\Platform\tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Modules\Platform\app\Models\Company;
use Modules\Platform\app\Models\LicenseKey;
use Tests\TestCase;
class LicenseKeyTest extends TestCase
{
use RefreshDatabase;
public function test_license_key_is_hashed_and_not_stored_plain(): void
{
$company = Company::create(['name' => 'Tenant A', 'slug' => 'tenant-a']);
$plainKey = LicenseKey::issueFor($company, 'Production licence', 'enterprise', 50);
$license = LicenseKey::firstOrFail();
$this->assertStringStartsWith('WMS-', $plainKey);
$this->assertNotSame($plainKey, $license->key_hash);
$this->assertSame(hash('sha256', $plainKey), $license->key_hash);
$this->assertTrue($company->fresh()->hasActiveLicense());
}
}
@@ -0,0 +1,50 @@
<?php
namespace Modules\Platform\tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Modules\Platform\app\Models\Company;
use Tests\TestCase;
class PlatformAccessTest extends TestCase
{
use RefreshDatabase;
public function test_mpba_platform_admin_can_access_admin_dashboard_without_licence(): void
{
$company = Company::create([
'name' => 'MPBA Limited',
'slug' => 'mpba-limited',
'is_system_company' => true,
'status' => 'active',
]);
$user = User::factory()->create([
'company_id' => $company->id,
'email' => 'mark.gregory@mpba.co.uk',
'is_platform_admin' => true,
'is_company_admin' => true,
]);
$this->actingAs($user)
->get('/admin')
->assertOk()
->assertSee('Platform administration');
}
public function test_non_system_company_requires_license(): void
{
$company = Company::create([
'name' => 'Customer Limited',
'slug' => 'customer-limited',
'status' => 'active',
]);
$user = User::factory()->create(['company_id' => $company->id]);
$this->actingAs($user)
->get('/dashboard')
->assertStatus(402);
}
}
@@ -0,0 +1,19 @@
<?php
namespace Modules\Warehouse\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class WarehouseServiceProvider extends ServiceProvider
{
/**
* Bootstrap module services.
*/
public function boot(): void
{
$this->loadViewsFrom(__DIR__ . '/../resources/views', 'warehouse');
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
Route::middleware('web')->group(__DIR__ . '/../routes/web.php');
}
}
@@ -0,0 +1,44 @@
<?php
namespace Modules\Warehouse\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Warehouse\app\Models\Warehouse;
class WarehouseController extends Controller
{
/**
* Display tenant warehouses.
*/
public function index()
{
return view('warehouse::warehouses.index', [
'warehouses' => Warehouse::withCount('validationRules')
->where('company_id', auth()->user()->company_id)
->orderBy('name')
->paginate(20),
]);
}
/**
* Create a tenant warehouse.
*/
public function store(Request $request)
{
$data = $request->validate([
'code' => ['required', 'string', 'max:50'],
'name' => ['required', 'string', 'max:255'],
'address' => ['nullable', 'string', 'max:2000'],
]);
Warehouse::create([
...$data,
'company_id' => $request->user()->company_id,
'status' => 'active',
'timezone' => $request->user()->company->timezone ?? 'Europe/London',
]);
return back()->with('status', 'Warehouse created.');
}
}
@@ -0,0 +1,24 @@
<?php
namespace Modules\Warehouse\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Modules\Warehouse\app\Models\Warehouse;
use Modules\Warehouse\app\Models\WarehouseValidationRule;
class WarehouseDashboardController extends Controller
{
/**
* Show tenant warehouse dashboard.
*/
public function __invoke()
{
$companyId = auth()->user()->company_id;
return view('warehouse::warehouses.dashboard', [
'warehousesCount' => Warehouse::where('company_id', $companyId)->count(),
'rulesCount' => WarehouseValidationRule::whereHas('warehouse', fn ($query) => $query->where('company_id', $companyId))->count(),
'warehouses' => Warehouse::withCount('validationRules')->where('company_id', $companyId)->orderBy('name')->get(),
]);
}
}
@@ -0,0 +1,56 @@
<?php
namespace Modules\Warehouse\app\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Modules\Warehouse\app\Models\Warehouse;
use Modules\Warehouse\app\Models\WarehouseValidationRule;
class WarehouseValidationRuleController extends Controller
{
/**
* Display warehouse validation rules.
*/
public function index()
{
$companyId = auth()->user()->company_id;
return view('warehouse::rules.index', [
'warehouses' => Warehouse::where('company_id', $companyId)->orderBy('name')->get(),
'rules' => WarehouseValidationRule::with('warehouse')
->whereHas('warehouse', fn ($query) => $query->where('company_id', $companyId))
->latest()
->paginate(20),
]);
}
/**
* Create a validation rule for a warehouse.
*/
public function store(Request $request)
{
$companyId = $request->user()->company_id;
$data = $request->validate([
'warehouse_id' => ['required', 'exists:warehouses,id'],
'name' => ['required', 'string', 'max:255'],
'rule_key' => ['required', 'string', 'max:100'],
'severity' => ['required', 'in:info,warning,error,blocker'],
'configuration' => ['nullable', 'json'],
]);
$warehouse = Warehouse::where('company_id', $companyId)->findOrFail($data['warehouse_id']);
WarehouseValidationRule::create([
'warehouse_id' => $warehouse->id,
'name' => $data['name'],
'rule_key' => $data['rule_key'],
'severity' => $data['severity'],
'is_active' => true,
'configuration' => $data['configuration'] ? json_decode($data['configuration'], true) : null,
]);
return back()->with('status', 'Validation rule created.');
}
}
@@ -0,0 +1,37 @@
<?php
namespace Modules\Warehouse\app\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
use Modules\Platform\app\Models\Company;
class Warehouse extends Model
{
protected $fillable = [
'company_id', 'uuid', 'code', 'name', 'status', 'address', 'timezone', 'settings',
];
protected $casts = ['settings' => 'array'];
protected static function booted(): void
{
static::creating(function (Warehouse $warehouse): void {
$warehouse->uuid ??= (string) Str::uuid();
$warehouse->status ??= 'active';
$warehouse->timezone ??= 'Europe/London';
});
}
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
public function validationRules(): HasMany
{
return $this->hasMany(WarehouseValidationRule::class);
}
}
@@ -0,0 +1,23 @@
<?php
namespace Modules\Warehouse\app\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class WarehouseValidationRule extends Model
{
protected $fillable = [
'warehouse_id', 'name', 'rule_key', 'severity', 'is_active', 'configuration',
];
protected $casts = [
'is_active' => 'boolean',
'configuration' => 'array',
];
public function warehouse(): BelongsTo
{
return $this->belongsTo(Warehouse::class);
}
}
@@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('warehouses', function (Blueprint $table): void {
$table->id();
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
$table->uuid('uuid')->unique();
$table->string('code');
$table->string('name');
$table->string('status')->default('active')->index();
$table->text('address')->nullable();
$table->string('timezone')->default('Europe/London');
$table->json('settings')->nullable();
$table->timestamps();
$table->unique(['company_id', 'code']);
});
Schema::create('warehouse_validation_rules', function (Blueprint $table): void {
$table->id();
$table->foreignId('warehouse_id')->constrained('warehouses')->cascadeOnDelete();
$table->string('name');
$table->string('rule_key')->index();
$table->string('severity')->default('error');
$table->boolean('is_active')->default(true)->index();
$table->json('configuration')->nullable();
$table->timestamps();
$table->unique(['warehouse_id', 'rule_key']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('warehouse_validation_rules');
Schema::dropIfExists('warehouses');
}
};
+1
View File
@@ -0,0 +1 @@
{"name":"Warehouse","alias":"warehouse","description":"Warehouse management, warehouse-specific validation rules and operational dashboard.","keywords":["wms","warehouse"],"priority":1,"providers":["Modules\\Warehouse\\Providers\\WarehouseServiceProvider"],"files":[]}
@@ -0,0 +1,18 @@
<x-layouts::app :title="__('Validation Rules')">
<div class="space-y-6">
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<h1 class="text-2xl font-bold">Warehouse validation rules</h1>
<form method="POST" action="{{ route('warehouse-rules.store') }}" class="mt-5 grid gap-4 md:grid-cols-6">
@csrf
<select name="warehouse_id" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950">@foreach($warehouses as $warehouse)<option value="{{ $warehouse->id }}">{{ $warehouse->name }}</option>@endforeach</select>
<input name="name" placeholder="Rule name" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950" required>
<input name="rule_key" placeholder="stock.location.required" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950" required>
<select name="severity" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950"><option>error</option><option>warning</option><option>blocker</option><option>info</option></select>
<input name="configuration" placeholder='{"min": 1}' class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950">
<button class="rounded-xl bg-zinc-900 px-4 py-2 font-semibold text-white dark:bg-white dark:text-zinc-900">Add rule</button>
</form>
</div>
<div class="overflow-x-auto rounded-2xl bg-white shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800"><table class="min-w-full text-sm"><thead><tr class="border-b"><th class="p-4 text-left">Warehouse</th><th class="p-4 text-left">Rule</th><th class="p-4 text-left">Key</th><th class="p-4 text-left">Severity</th></tr></thead><tbody>@foreach($rules as $rule)<tr class="border-b border-zinc-100 dark:border-zinc-800"><td class="p-4">{{ $rule->warehouse->name }}</td><td class="p-4 font-semibold">{{ $rule->name }}</td><td class="p-4"><code>{{ $rule->rule_key }}</code></td><td class="p-4">{{ $rule->severity }}</td></tr>@endforeach</tbody></table></div>
{{ $rules->links() }}
</div>
</x-layouts::app>
@@ -0,0 +1,27 @@
<x-layouts::app :title="__('WMS Dashboard')">
<div class="space-y-6">
<section class="overflow-hidden rounded-3xl bg-gradient-to-br from-slate-950 via-slate-900 to-cyan-950 p-6 text-white shadow-xl sm:p-8">
<div class="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div>
<p class="text-sm font-semibold uppercase tracking-[0.3em] text-cyan-300">Warehouse management</p>
<h1 class="mt-3 text-3xl font-bold tracking-tight sm:text-5xl">{{ auth()->user()->company->name }}</h1>
<p class="mt-3 max-w-2xl text-slate-300">High-performance tenant-isolated warehouse operations with per-warehouse validation and licence-aware access.</p>
</div>
<div class="grid grid-cols-2 gap-3 sm:min-w-80">
<div class="rounded-2xl bg-white/10 p-4 backdrop-blur"><p class="text-sm text-slate-300">Warehouses</p><p class="mt-2 text-3xl font-bold">{{ $warehousesCount }}</p></div>
<div class="rounded-2xl bg-white/10 p-4 backdrop-blur"><p class="text-sm text-slate-300">Rules</p><p class="mt-2 text-3xl font-bold">{{ $rulesCount }}</p></div>
</div>
</div>
</section>
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
@forelse($warehouses as $warehouse)
<article class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
<div class="flex items-start justify-between gap-4"><div><p class="text-sm font-semibold text-cyan-600">{{ $warehouse->code }}</p><h2 class="mt-1 text-xl font-bold">{{ $warehouse->name }}</h2></div><span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-800">{{ $warehouse->status }}</span></div>
<p class="mt-4 text-sm text-zinc-500">{{ $warehouse->validation_rules_count }} validation rules configured.</p>
</article>
@empty
<article class="rounded-2xl border border-dashed border-zinc-300 bg-white p-8 text-center dark:border-zinc-800 dark:bg-zinc-900 md:col-span-2 xl:col-span-3"><h2 class="text-xl font-bold">Create your first warehouse</h2><p class="mt-2 text-zinc-500">Warehouses are isolated to your company tenant.</p><a href="{{ route('warehouses.index') }}" class="mt-5 inline-flex rounded-xl bg-zinc-900 px-4 py-2 font-semibold text-white dark:bg-white dark:text-zinc-900">Add warehouse</a></article>
@endforelse
</section>
</div>
</x-layouts::app>
@@ -0,0 +1,20 @@
<x-layouts::app :title="__('Warehouses')">
<div class="space-y-6">
<div class="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
<h1 class="text-2xl font-bold">Warehouses</h1>
<form method="POST" action="{{ route('warehouses.store') }}" class="mt-5 grid gap-4 md:grid-cols-4">
@csrf
<input name="code" placeholder="Code" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950" required>
<input name="name" placeholder="Warehouse name" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950" required>
<input name="address" placeholder="Address" class="rounded-xl border-zinc-300 dark:border-zinc-700 dark:bg-zinc-950">
<button class="rounded-xl bg-zinc-900 px-4 py-2 font-semibold text-white dark:bg-white dark:text-zinc-900">Create</button>
</form>
</div>
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
@foreach($warehouses as $warehouse)
<article class="rounded-2xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900"><p class="text-sm text-cyan-600">{{ $warehouse->code }}</p><h2 class="mt-1 text-xl font-bold">{{ $warehouse->name }}</h2><p class="mt-3 text-sm text-zinc-500">{{ $warehouse->validation_rules_count }} rules</p></article>
@endforeach
</div>
{{ $warehouses->links() }}
</div>
</x-layouts::app>
+15
View File
@@ -0,0 +1,15 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\Warehouse\app\Http\Controllers\WarehouseController;
use Modules\Warehouse\app\Http\Controllers\WarehouseDashboardController;
use Modules\Warehouse\app\Http\Controllers\WarehouseValidationRuleController;
Route::middleware(['auth', 'verified', 'tenant.assigned', 'tenant.licensed'])
->group(function (): void {
Route::get('/dashboard', WarehouseDashboardController::class)->name('dashboard');
Route::get('/warehouses', [WarehouseController::class, 'index'])->name('warehouses.index');
Route::post('/warehouses', [WarehouseController::class, 'store'])->name('warehouses.store');
Route::get('/warehouse-rules', [WarehouseValidationRuleController::class, 'index'])->name('warehouse-rules.index');
Route::post('/warehouse-rules', [WarehouseValidationRuleController::class, 'store'])->name('warehouse-rules.store');
});
@@ -0,0 +1,32 @@
<?php
namespace Modules\Warehouse\tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Modules\Platform\app\Models\Company;
use Modules\Platform\app\Models\LicenseKey;
use Modules\Warehouse\app\Models\Warehouse;
use Tests\TestCase;
class WarehouseIsolationTest extends TestCase
{
use RefreshDatabase;
public function test_company_user_only_sees_their_warehouses(): void
{
$companyA = Company::create(['name' => 'A Ltd', 'slug' => 'a-ltd']);
$companyB = Company::create(['name' => 'B Ltd', 'slug' => 'b-ltd']);
LicenseKey::issueFor($companyA, 'A licence');
$user = User::factory()->create(['company_id' => $companyA->id]);
Warehouse::create(['company_id' => $companyA->id, 'code' => 'A1', 'name' => 'A Warehouse']);
Warehouse::create(['company_id' => $companyB->id, 'code' => 'B1', 'name' => 'B Warehouse']);
$this->actingAs($user)
->get('/warehouses')
->assertOk()
->assertSee('A Warehouse')
->assertDontSee('B Warehouse');
}
}
+25 -7
View File
@@ -5,29 +5,47 @@ namespace App\Actions\Fortify;
use App\Concerns\PasswordValidationRules;
use App\Concerns\ProfileValidationRules;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Laravel\Fortify\Contracts\CreatesNewUsers;
use Modules\Platform\app\Models\Company;
class CreateNewUser implements CreatesNewUsers
{
use PasswordValidationRules, ProfileValidationRules;
/**
* Validate and create a newly registered user.
* Validate and create a newly registered user with an isolated company tenant.
*
* @param array<string, string> $input
* @param array<string, string> $input
*/
public function create(array $input): User
{
Validator::make($input, [
...$this->profileRules(),
'password' => $this->passwordRules(),
'company_name' => ['nullable', 'string', 'max:255'],
])->validate();
return User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => $input['password'],
]);
return DB::transaction(function () use ($input): User {
$companyName = $input['company_name'] ?? ($input['name'] . ' Company');
$company = Company::create([
'name' => $companyName,
'slug' => Str::slug($companyName . '-' . Str::random(6)),
'status' => 'active',
'timezone' => 'Europe/London',
]);
return User::create([
'company_id' => $company->id,
'name' => $input['name'],
'email' => $input['email'],
'password' => $input['password'],
'is_company_admin' => true,
'status' => 'active',
]);
});
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Actions\Fortify;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Validation\Rules\Password;
trait PasswordValidationRules
{
/**
* Get the validation rules used to validate passwords.
*
* @return array<int, Rule|array<mixed>|string>
*/
protected function passwordRules(): array
{
return ['required', 'string', Password::default(), 'confirmed'];
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
class UpdateUserPassword implements UpdatesUserPasswords
{
use PasswordValidationRules;
/**
* Validate and update the user's password.
*
* @param array<string, string> $input
*
* @throws ValidationException
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'current_password' => ['required', 'string', 'current_password:web'],
'password' => $this->passwordRules(),
], [
'current_password.current_password' => __('The provided password does not match your current password.'),
])->validateWithBag('updatePassword');
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}
@@ -0,0 +1,61 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
{
/**
* Validate and update the given user's profile information.
*
* @param array<string, string> $input
*
* @throws ValidationException
*/
public function update(User $user, array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique('users')->ignore($user->id),
],
])->validateWithBag('updateProfileInformation');
if ($input['email'] !== $user->email &&
$user instanceof MustVerifyEmail) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
])->save();
}
}
/**
* Update the given verified user's profile information.
*
* @param array<string, string> $input
*/
protected function updateVerifiedUser(User $user, array $input): void
{
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
'email_verified_at' => null,
])->save();
$user->sendEmailVerificationNotification();
}
}
+8 -1
View File
@@ -5,6 +5,7 @@ namespace App\Livewire\Settings;
use App\Concerns\PasswordValidationRules;
use App\Livewire\Actions\Logout;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema;
use Livewire\Component;
class DeleteUserForm extends Component
@@ -22,7 +23,13 @@ class DeleteUserForm extends Component
'password' => $this->currentPasswordRules(),
]);
tap(Auth::user(), $logout(...))->delete();
$user = Auth::user();
if ($user && Schema::hasTable(config('permission.table_names.model_has_roles', 'model_has_roles'))) {
$user->syncRoles([]);
}
tap($user, $logout(...))->delete();
$this->redirect('/', navigate: true);
}
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace App\Models;
class Company extends \Modules\Platform\app\Models\Company
{
}
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace App\Models;
class LicenseKey extends \Modules\Platform\app\Models\LicenseKey
{
}
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace App\Models;
class Payment extends \Modules\Platform\app\Models\Payment
{
}
+22 -15
View File
@@ -2,47 +2,54 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Laravel\Fortify\Contracts\PasskeyUser;
use Laravel\Fortify\PasskeyAuthenticatable;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Spatie\Permission\Traits\HasRoles;
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
class User extends Authenticatable implements PasskeyUser
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
use HasFactory, HasRoles, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
protected $fillable = [
'company_id', 'name', 'email', 'password', 'is_platform_admin', 'is_company_admin', 'status',
];
protected $hidden = ['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_platform_admin' => 'boolean',
'is_company_admin' => 'boolean',
];
}
/**
* Get the user's initials
*/
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
public function initials(): string
{
return Str::of($this->name)
->explode(' ')
->take(2)
->map(fn ($word) => Str::substr($word, 0, 1))
->map(fn ($word) => Str::substr((string) $word, 0, 1))
->implode('');
}
public function isMpbaPlatformAdmin(): bool
{
return $this->is_platform_admin && $this->company?->is_system_company === true;
}
}
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace App\Models;
class Warehouse extends \Modules\Warehouse\app\Models\Warehouse
{
}
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace App\Models;
class WarehouseValidationRule extends \Modules\Warehouse\app\Models\WarehouseValidationRule
{
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Laravel\Horizon\Horizon;
use Laravel\Horizon\HorizonApplicationServiceProvider;
class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
parent::boot();
// Horizon::routeSmsNotificationsTo('15556667777');
// Horizon::routeMailNotificationsTo('example@example.com');
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
}
/**
* Register the Horizon gate.
*
* This gate determines who can access Horizon in non-local environments.
*/
protected function gate(): void
{
Gate::define('viewHorizon', function ($user = null) {
return in_array(optional($user)->email, [
//
]);
});
}
}
+148
View File
@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Events;
use Stancl\Tenancy\Jobs;
use Stancl\Tenancy\Listeners;
use Stancl\Tenancy\Middleware;
class TenancyServiceProvider extends ServiceProvider
{
// By default, no namespace is used to support the callable array syntax.
public static string $controllerNamespace = '';
public function events()
{
return [
// Tenant events
Events\CreatingTenant::class => [],
Events\TenantCreated::class => [
JobPipeline::make([
Jobs\CreateDatabase::class,
Jobs\MigrateDatabase::class,
// Jobs\SeedDatabase::class,
// Your own jobs to prepare the tenant.
// Provision API keys, create S3 buckets, anything you want!
])->send(function (Events\TenantCreated $event) {
return $event->tenant;
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
],
Events\SavingTenant::class => [],
Events\TenantSaved::class => [],
Events\UpdatingTenant::class => [],
Events\TenantUpdated::class => [],
Events\DeletingTenant::class => [],
Events\TenantDeleted::class => [
JobPipeline::make([
Jobs\DeleteDatabase::class,
])->send(function (Events\TenantDeleted $event) {
return $event->tenant;
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
],
// Domain events
Events\CreatingDomain::class => [],
Events\DomainCreated::class => [],
Events\SavingDomain::class => [],
Events\DomainSaved::class => [],
Events\UpdatingDomain::class => [],
Events\DomainUpdated::class => [],
Events\DeletingDomain::class => [],
Events\DomainDeleted::class => [],
// Database events
Events\DatabaseCreated::class => [],
Events\DatabaseMigrated::class => [],
Events\DatabaseSeeded::class => [],
Events\DatabaseRolledBack::class => [],
Events\DatabaseDeleted::class => [],
// Tenancy events
Events\InitializingTenancy::class => [],
Events\TenancyInitialized::class => [
Listeners\BootstrapTenancy::class,
],
Events\EndingTenancy::class => [],
Events\TenancyEnded::class => [
Listeners\RevertToCentralContext::class,
],
Events\BootstrappingTenancy::class => [],
Events\TenancyBootstrapped::class => [],
Events\RevertingToCentralContext::class => [],
Events\RevertedToCentralContext::class => [],
// Resource syncing
Events\SyncedResourceSaved::class => [
Listeners\UpdateSyncedResource::class,
],
// Fired only when a synced resource is changed in a different DB than the origin DB (to avoid infinite loops)
Events\SyncedResourceChangedInForeignDatabase::class => [],
];
}
public function register()
{
//
}
public function boot()
{
$this->bootEvents();
$this->mapRoutes();
$this->makeTenancyMiddlewareHighestPriority();
}
protected function bootEvents()
{
foreach ($this->events() as $event => $listeners) {
foreach ($listeners as $listener) {
if ($listener instanceof JobPipeline) {
$listener = $listener->toListener();
}
Event::listen($event, $listener);
}
}
}
protected function mapRoutes()
{
$this->app->booted(function () {
if (file_exists(base_path('routes/tenant.php'))) {
Route::namespace(static::$controllerNamespace)
->group(base_path('routes/tenant.php'));
}
});
}
protected function makeTenancyMiddlewareHighestPriority()
{
$tenancyMiddleware = [
// Even higher priority than the initialization middleware
Middleware\PreventAccessFromCentralDomains::class,
Middleware\InitializeTenancyByDomain::class,
Middleware\InitializeTenancyBySubdomain::class,
Middleware\InitializeTenancyByDomainOrSubdomain::class,
Middleware\InitializeTenancyByPath::class,
Middleware\InitializeTenancyByRequestData::class,
];
foreach (array_reverse($tenancyMiddleware) as $middleware) {
$this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware);
}
}
}
Executable → Regular
View File
+8 -1
View File
@@ -12,7 +12,14 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
$environment = $_ENV['APP_ENV']
?? $_SERVER['APP_ENV']
?? getenv('APP_ENV')
?: null;
if ($environment === 'testing') {
$middleware->validateCsrfTokens(except: ['*']);
}
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(
+6
View File
@@ -2,8 +2,14 @@
use App\Providers\AppServiceProvider;
use App\Providers\FortifyServiceProvider;
use Modules\InternalApi\Providers\InternalApiServiceProvider;
use Modules\Platform\Providers\PlatformServiceProvider;
use Modules\Warehouse\Providers\WarehouseServiceProvider;
return [
AppServiceProvider::class,
FortifyServiceProvider::class,
PlatformServiceProvider::class,
WarehouseServiceProvider::class,
InternalApiServiceProvider::class,
];
+11
View File
@@ -10,8 +10,12 @@
"license": "MIT",
"require": {
"php": "^8.3",
"ayeo/gs1_barcode": "^1.0",
"barryvdh/laravel-dompdf": "^3.1",
"filament/filament": "^5.6",
"filament/tables": "^5.6",
"guzzlehttp/guzzle": "^7.11",
"laravel-lang/common": "^6.8",
"laravel/breeze": "^2.4",
"laravel/chisel": "^0.1.0",
"laravel/fortify": "^1.37.2",
@@ -23,9 +27,16 @@
"livewire/blaze": "^1.0",
"livewire/flux": "^2.14",
"livewire/livewire": "^4.1",
"mikehaertl/php-pdftk": "*",
"mpba/laravel-modules": "dev-main",
"phpoffice/phpspreadsheet": "*",
"picqer/php-barcode-generator": "*",
"predis/predis": "^3.5",
"ramsey/uuid": "^4.9",
"robgridley/zebra": "*",
"setasign/fpdf": "*",
"setasign/fpdi": "*",
"smalot/pdfparser": "*",
"spatie/laravel-medialibrary": "^11.23",
"spatie/laravel-permission": "^8.0",
"stancl/tenancy": "^3.10",
+383
View File
@@ -0,0 +1,383 @@
<?php
use Spatie\Backup\Notifications\Notifiable;
use Spatie\Backup\Notifications\Notifications\BackupHasFailedNotification;
use Spatie\Backup\Notifications\Notifications\BackupWasSuccessfulNotification;
use Spatie\Backup\Notifications\Notifications\CleanupHasFailedNotification;
use Spatie\Backup\Notifications\Notifications\CleanupWasSuccessfulNotification;
use Spatie\Backup\Notifications\Notifications\HealthyBackupWasFoundNotification;
use Spatie\Backup\Notifications\Notifications\UnhealthyBackupWasFoundNotification;
use Spatie\Backup\Tasks\Cleanup\Strategies\DefaultStrategy;
use Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumAgeInDays;
use Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumStorageInMegabytes;
return [
'backup' => [
/*
* The name of this application. You can use this name to monitor
* the backups.
*/
'name' => env('APP_NAME', 'laravel-backup'),
'source' => [
'files' => [
/*
* The list of directories and files that will be included in the backup.
*/
'include' => [
base_path(),
// storage_path(), // Include if you use zero downtime deployments and don't follow symlinks
],
/*
* These directories and files will be excluded from the backup.
*
* Directories used by the backup process will automatically be excluded.
*/
'exclude' => [
base_path('vendor'),
base_path('node_modules'),
storage_path('framework'),
],
/*
* Determines if symlinks should be followed.
*/
'follow_links' => false,
/*
* Determines if it should avoid unreadable folders.
*/
'ignore_unreadable_directories' => false,
/*
* This path is used to make directories in resulting zip-file relative
* Set to `null` to include complete absolute path
* Example: base_path()
*/
'relative_path' => null,
],
/*
* The names of the connections to the databases that should be backed up
* MySQL, PostgreSQL, SQLite and Mongo databases are supported.
*
* The content of the database dump may be customized for each connection
* by adding a 'dump' key to the connection settings in config/database.php.
* E.g.
* 'mysql' => [
* ...
* 'dump' => [
* 'exclude_tables' => [
* 'table_to_exclude_from_backup',
* 'another_table_to_exclude'
* ]
* ],
* ],
*
* If you are using only InnoDB tables on a MySQL server, you can
* also supply the useSingleTransaction option to avoid table locking.
*
* E.g.
* 'mysql' => [
* ...
* 'dump' => [
* 'useSingleTransaction' => true,
* ],
* ],
*
* For a complete list of available customization options, see https://github.com/spatie/db-dumper
*/
'databases' => [
env('DB_CONNECTION', 'mysql'),
],
],
/*
* The database dump can be compressed to decrease disk space usage.
*
* Out of the box Laravel-backup supplies
* Spatie\DbDumper\Compressors\GzipCompressor::class.
*
* You can also create custom compressor. More info on that here:
* https://github.com/spatie/db-dumper#using-compression
*
* If you do not want any compressor at all, set it to null.
*/
'database_dump_compressor' => null,
/*
* If specified, the database dumped file name will contain a timestamp (e.g.: 'Y-m-d-H-i-s').
*/
'database_dump_file_timestamp_format' => null,
/*
* The base of the dump filename, either 'database' or 'connection'
*
* If 'database' (default), the dumped filename will contain the database name.
* If 'connection', the dumped filename will contain the connection name.
*/
'database_dump_filename_base' => 'database',
/*
* The file extension used for the database dump files.
*
* If not specified, the file extension will be .archive for MongoDB and .sql for all other databases
* The file extension should be specified without a leading .
*/
'database_dump_file_extension' => '',
'destination' => [
/*
* The compression algorithm to be used for creating the zip archive.
*
* If backing up only database, you may choose gzip compression for db dump and no compression at zip.
*
* Some common algorithms are listed below:
* ZipArchive::CM_STORE (no compression at all; set 0 as compression level)
* ZipArchive::CM_DEFAULT
* ZipArchive::CM_DEFLATE
* ZipArchive::CM_BZIP2
* ZipArchive::CM_XZ
*
* For more check https://www.php.net/manual/zip.constants.php and confirm it's supported by your system.
*/
'compression_method' => ZipArchive::CM_DEFAULT,
/*
* The compression level corresponding to the used algorithm; an integer between 0 and 9.
*
* Check supported levels for the chosen algorithm, usually 1 means the fastest and weakest compression,
* while 9 the slowest and strongest one.
*
* Setting of 0 for some algorithms may switch to the strongest compression.
*/
'compression_level' => 9,
/*
* The filename prefix used for the backup zip file.
*/
'filename_prefix' => '',
/*
* The disk names on which the backups will be stored.
*/
'disks' => [
'local',
],
/*
* Determines whether to allow backups to continue when some targets fail instead of failing completely.
*/
'continue_on_failure' => false,
],
/*
* The directory where the temporary files will be stored.
*/
'temporary_directory' => storage_path('app/backup-temp'),
/*
* The password to be used for archive encryption.
* Set to `null` to disable encryption.
*/
'password' => env('BACKUP_ARCHIVE_PASSWORD'),
/*
* The encryption algorithm to be used for archive encryption.
* Set to 'none' to disable encryption.
*
* Supported: 'none', 'default', 'aes128', 'aes192', 'aes256'
*
* When set to 'default', we'll use AES-256 if available on your system.
*/
'encryption' => 'default',
/*
* After creating the zip, verify it can be opened and contains files.
* Recommended for critical backups but adds a small overhead.
*/
'verify_backup' => false,
/*
* The number of attempts, in case the backup command encounters an exception
*/
'tries' => 1,
/*
* The number of seconds to wait before attempting a new backup if the previous try failed
* Set to `0` for none
*/
'retry_delay' => 0,
],
/*
* You can get notified when specific events occur. Out of the box you can use 'mail' and 'slack'.
* For Slack you need to install laravel/slack-notification-channel.
*
* You can also use your own notification classes, just make sure the class is named after one of
* the `Spatie\Backup\Notifications\Notifications` classes.
*/
'notifications' => [
'notifications' => [
BackupHasFailedNotification::class => ['mail'],
UnhealthyBackupWasFoundNotification::class => ['mail'],
CleanupHasFailedNotification::class => ['mail'],
BackupWasSuccessfulNotification::class => ['mail'],
HealthyBackupWasFoundNotification::class => ['mail'],
CleanupWasSuccessfulNotification::class => ['mail'],
],
/*
* Here you can specify the notifiable to which the notifications should be sent. The default
* notifiable will use the variables specified in this config file.
*/
'notifiable' => Notifiable::class,
'mail' => [
'to' => 'your@example.com',
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
],
'slack' => [
'webhook_url' => '',
/*
* If this is set to null the default channel of the webhook will be used.
*/
'channel' => null,
'username' => null,
'icon' => null,
],
'discord' => [
'webhook_url' => '',
/*
* If this is an empty string, the name field on the webhook will be used.
*/
'username' => '',
/*
* If this is an empty string, the avatar on the webhook will be used.
*/
'avatar_url' => '',
],
/*
* A generic webhook channel that POSTs JSON to a URL.
* Useful for Mattermost, Microsoft Teams, or custom integrations.
*/
'webhook' => [
'url' => '',
],
],
/*
* The log channel used for backup activity messages.
*
* Set to a channel name defined in config/logging.php to use that channel.
* Set to false to disable backup logging entirely.
* Set to null to use the default log channel.
*/
'log_channel' => null,
/*
* Here you can specify which backups should be monitored.
* If a backup does not meet the specified requirements the
* UnHealthyBackupWasFound event will be fired.
*/
'monitor_backups' => [
[
'name' => env('APP_NAME', 'laravel-backup'),
'disks' => ['local'],
'health_checks' => [
MaximumAgeInDays::class => 1,
MaximumStorageInMegabytes::class => 5000,
],
],
/*
[
'name' => 'name of the second app',
'disks' => ['local', 's3'],
'health_checks' => [
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumAgeInDays::class => 1,
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumStorageInMegabytes::class => 5000,
],
],
*/
],
'cleanup' => [
/*
* The strategy that will be used to cleanup old backups. The default strategy
* will keep all backups for a certain amount of days. After that period only
* a daily backup will be kept. After that period only weekly backups will
* be kept and so on.
*
* No matter how you configure it the default strategy will never
* delete the newest backup.
*/
'strategy' => DefaultStrategy::class,
'default_strategy' => [
/*
* The number of days for which backups must be kept.
*/
'keep_all_backups_for_days' => 7,
/*
* After the "keep_all_backups_for_days" period is over, the most recent backup
* of that day will be kept. Older backups within the same day will be removed.
* If you create backups only once a day, no backups will be removed yet.
*/
'keep_daily_backups_for_days' => 16,
/*
* After the "keep_daily_backups_for_days" period is over, the most recent backup
* of that week will be kept. Older backups within the same week will be removed.
* If you create backups only once a week, no backups will be removed yet.
*/
'keep_weekly_backups_for_weeks' => 8,
/*
* After the "keep_weekly_backups_for_weeks" period is over, the most recent backup
* of that month will be kept. Older backups within the same month will be removed.
*/
'keep_monthly_backups_for_months' => 4,
/*
* After the "keep_monthly_backups_for_months" period is over, the most recent backup
* of that year will be kept. Older backups within the same year will be removed.
*/
'keep_yearly_backups_for_years' => 2,
/*
* After cleaning up the backups remove the oldest backup until
* this amount of megabytes has been reached.
* Set null for unlimited size.
*/
'delete_oldest_backups_when_using_more_megabytes_than' => 5000,
],
/*
* The number of attempts, in case the cleanup command encounters an exception
*/
'tries' => 1,
/*
* The number of seconds to wait before attempting a new cleanup if the previous try failed
* Set to `0` for none
*/
'retry_delay' => 0,
],
];
+57
View File
@@ -0,0 +1,57 @@
<?php
return [
/*
|-----------------------------------------------------------------
| Default Prefix
|-----------------------------------------------------------------
|
| This config option allows you to define a default prefix for
| your icons. The dash separator will be applied automatically
| to every icon name. It's required and needs to be unique.
|
*/
'prefix' => 'heroicon',
/*
|-----------------------------------------------------------------
| Fallback Icon
|-----------------------------------------------------------------
|
| This config option allows you to define a fallback
| icon when an icon in this set cannot be found.
|
*/
'fallback' => '',
/*
|-----------------------------------------------------------------
| Default Set Classes
|-----------------------------------------------------------------
|
| This config option allows you to define some classes which
| will be applied by default to all icons within this set.
|
*/
'class' => '',
/*
|-----------------------------------------------------------------
| Default Set Attributes
|-----------------------------------------------------------------
|
| This config option allows you to define some attributes which
| will be applied by default to all icons within this set.
|
*/
'attributes' => [
// 'width' => 50,
// 'height' => 50,
],
];
+183
View File
@@ -0,0 +1,183 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Icons Sets
|--------------------------------------------------------------------------
|
| With this config option you can define a couple of
| default icon sets. Provide a key name for your icon
| set and a combination from the options below.
|
*/
'sets' => [
// 'default' => [
//
// /*
// |-----------------------------------------------------------------
// | Icons Path
// |-----------------------------------------------------------------
// |
// | Provide the relative path from your app root to your SVG icons
// | directory. Icons are loaded recursively so there's no need to
// | list every sub-directory.
// |
// | Relative to the disk root when the disk option is set.
// |
// */
//
// 'path' => 'resources/svg',
//
// /*
// |-----------------------------------------------------------------
// | Filesystem Disk
// |-----------------------------------------------------------------
// |
// | Optionally, provide a specific filesystem disk to read
// | icons from. When defining a disk, the "path" option
// | starts relatively from the disk root.
// |
// */
//
// 'disk' => '',
//
// /*
// |-----------------------------------------------------------------
// | Default Prefix
// |-----------------------------------------------------------------
// |
// | This config option allows you to define a default prefix for
// | your icons. The dash separator will be applied automatically
// | to every icon name. It's required and needs to be unique.
// |
// */
//
// 'prefix' => 'icon',
//
// /*
// |-----------------------------------------------------------------
// | Fallback Icon
// |-----------------------------------------------------------------
// |
// | This config option allows you to define a fallback
// | icon when an icon in this set cannot be found.
// |
// */
//
// 'fallback' => '',
//
// /*
// |-----------------------------------------------------------------
// | Default Set Classes
// |-----------------------------------------------------------------
// |
// | This config option allows you to define some classes which
// | will be applied by default to all icons within this set.
// |
// */
//
// 'class' => '',
//
// /*
// |-----------------------------------------------------------------
// | Default Set Attributes
// |-----------------------------------------------------------------
// |
// | This config option allows you to define some attributes which
// | will be applied by default to all icons within this set.
// |
// */
//
// 'attributes' => [
// // 'width' => 50,
// // 'height' => 50,
// ],
//
// ],
],
/*
|--------------------------------------------------------------------------
| Global Default Classes
|--------------------------------------------------------------------------
|
| This config option allows you to define some classes which
| will be applied by default to all icons.
|
*/
'class' => '',
/*
|--------------------------------------------------------------------------
| Global Default Attributes
|--------------------------------------------------------------------------
|
| This config option allows you to define some attributes which
| will be applied by default to all icons.
|
*/
'attributes' => [
// 'width' => 50,
// 'height' => 50,
],
/*
|--------------------------------------------------------------------------
| Global Fallback Icon
|--------------------------------------------------------------------------
|
| This config option allows you to define a global fallback
| icon when an icon in any set cannot be found. It can
| reference any icon from any configured set.
|
*/
'fallback' => '',
/*
|--------------------------------------------------------------------------
| Components
|--------------------------------------------------------------------------
|
| These config options allow you to define some
| settings related to Blade Components.
|
*/
'components' => [
/*
|----------------------------------------------------------------------
| Disable Components
|----------------------------------------------------------------------
|
| This config option allows you to disable Blade components
| completely. It's useful to avoid performance problems
| when working with large icon libraries.
|
*/
'disabled' => false,
/*
|----------------------------------------------------------------------
| Default Icon Component Name
|----------------------------------------------------------------------
|
| This config option allows you to define the name
| for the default Icon class component.
|
*/
'default' => 'icon',
],
];
+31
View File
@@ -0,0 +1,31 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Enable Blaze
|--------------------------------------------------------------------------
|
| When set to false, Blaze will skip all compilation and optimization.
| Templates will be rendered using standard Blade without any Blaze
| processing. This is useful for debugging or disabling Blaze in
| specific environments.
|
*/
'enabled' => env('BLAZE_ENABLED', true),
/*
|--------------------------------------------------------------------------
| Debug Mode
|--------------------------------------------------------------------------
|
| When enabled, Blaze will register the debugger middleware and output
| additional diagnostic information about the compilation pipeline.
|
*/
'debug' => env('BLAZE_DEBUG', false),
];
+52
View File
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
return [
/*
|--------------------------------------------------------------------------
| Boost Master Switch
|--------------------------------------------------------------------------
|
| This option may be used to disable all Boost functionality - which
| will prevent Boost's routes from being registered and will also
| disable Boost's browser logging functionality from operating.
|
*/
'enabled' => env('BOOST_ENABLED', true),
/*
|--------------------------------------------------------------------------
| Boost Browser Logs Watcher
|--------------------------------------------------------------------------
|
| The following option may be used to enable or disable the browser logs
| watcher feature within Laravel Boost. The log watcher will read any
| errors within the browser's console to give Boost better context.
|
*/
'browser_logs_watcher' => env('BOOST_BROWSER_LOGS_WATCHER', true),
/*
|--------------------------------------------------------------------------
| Boost Executables Paths
|--------------------------------------------------------------------------
|
| These options allow you to specify custom paths for the executables that
| Boost uses. When configured, they take precedence over the automatic
| discovery mechanism. Leave empty to use defaults from your $PATH.
|
*/
'executable_paths' => [
'php' => env('BOOST_PHP_EXECUTABLE_PATH'),
'composer' => env('BOOST_COMPOSER_EXECUTABLE_PATH'),
'npm' => env('BOOST_NPM_EXECUTABLE_PATH'),
'vendor_bin' => env('BOOST_VENDOR_BIN_EXECUTABLE_PATH'),
'current_directory' => env('BOOST_CURRENT_DIRECTORY_EXECUTABLE_PATH'),
],
];
+91
View File
@@ -0,0 +1,91 @@
<?php
return [
/*
* Namespaces used by the generator.
*/
'namespace' => [
/*
* Base namespace/directory to create the new file.
* This is appended on default Laravel namespace.
* Usage: php artisan datatables:make User
* Output: App\DataTables\UserDataTable
* With Model: App\User (default model)
* Export filename: users_timestamp
*/
'base' => 'DataTables',
/*
* Base namespace/directory where your model's are located.
* This is appended on default Laravel namespace.
* Usage: php artisan datatables:make Post --model
* Output: App\DataTables\PostDataTable
* With Model: App\Post
* Export filename: posts_timestamp
*/
'model' => 'App\\Models',
],
/*
* Set Custom stub folder
*/
// 'stub' => '/resources/custom_stub',
/*
* PDF generator to be used when converting the table to pdf.
* Available generators: excel, snappy
* Snappy package: barryvdh/laravel-snappy
* Excel package: maatwebsite/excel
*/
'pdf_generator' => 'snappy',
/*
* Snappy PDF options.
*/
'snappy' => [
'options' => [
'no-outline' => true,
'margin-left' => '0',
'margin-right' => '0',
'margin-top' => '10mm',
'margin-bottom' => '10mm',
],
'orientation' => 'landscape',
],
/*
* Default html builder parameters.
*/
'parameters' => [
'dom' => 'Bfrtip',
'order' => [[0, 'desc']],
'buttons' => [
'excel',
'csv',
'pdf',
'print',
'reset',
'reload',
],
],
/*
* Generator command default options value.
*/
'generator' => [
/*
* Default columns to generate when not set.
*/
'columns' => 'id,add your columns,created_at,updated_at',
/*
* Default buttons to generate when not set.
*/
'buttons' => 'excel,csv,pdf,print,reset,reload',
/*
* Default DOM to generate when not set.
*/
'dom' => 'Bfrtip',
],
];
+120
View File
@@ -0,0 +1,120 @@
<?php
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
return [
/*
|--------------------------------------------------------------------------
| Method
|--------------------------------------------------------------------------
|
| Method to use to iterate with the query results.
| Options: lazy, cursor
|
| @link https://laravel.com/docs/eloquent#cursors
| @link https://laravel.com/docs/eloquent#chunking-using-lazy-collections
|
*/
'method' => 'lazy',
/*
|--------------------------------------------------------------------------
| Chunk Size
|--------------------------------------------------------------------------
|
| Chunk size to be used when using lazy method.
|
*/
'chunk' => 1000,
/*
|--------------------------------------------------------------------------
| Export filesystem disk
|--------------------------------------------------------------------------
|
| Export filesystem disk where generated files will be stored.
|
*/
'disk' => 'local',
/*
|--------------------------------------------------------------------------
| Use S3 for final file destination
|--------------------------------------------------------------------------
|
| After generating the file locally, it can be uploaded to s3.
|
*/
's3_disk' => '',
/*
|--------------------------------------------------------------------------
| Mail from address
|--------------------------------------------------------------------------
|
| Will be used to email report from this address.
|
*/
'mail_from' => env('MAIL_FROM_ADDRESS', ''),
/*
|--------------------------------------------------------------------------
| Default Date Format
|--------------------------------------------------------------------------
|
| Default export format for date.
|
*/
'default_date_format' => 'yyyy-mm-dd',
/*
|--------------------------------------------------------------------------
| Valid Date Formats
|--------------------------------------------------------------------------
|
| List of valid date formats to be used for auto-detection.
|
*/
'date_formats' => [
'mm/dd/yyyy',
...NumberFormat::DATE_TIME_OR_DATETIME_ARRAY,
...NumberFormat::TIME_OR_DATETIME_ARRAY,
],
/*
|--------------------------------------------------------------------------
| Valid Text Formats
|--------------------------------------------------------------------------
|
| List of valid text formats to be used.
|
*/
'text_formats' => [
NumberFormat::FORMAT_TEXT,
NumberFormat::FORMAT_GENERAL,
],
/*
|--------------------------------------------------------------------------
| Purge Options
|--------------------------------------------------------------------------
|
| Purge all exported by purge.days old files.
|
*/
'purge' => [
'days' => 1,
],
/*
* --------------------------------------------------------------------------
* Export Queue Name
* --------------------------------------------------------------------------
*
* The name of the queue connection to use for processing the export batch jobs.
* If this is set to null, the jobs will be pushed to the application's default queue.
*
*/
'queue' => env('DATATABLES_EXPORT_QUEUE', null),
];
+15
View File
@@ -0,0 +1,15 @@
<?php
use League\Fractal\Serializer\DataArraySerializer;
return [
/*
* Request key name to parse includes on fractal.
*/
'includes' => 'include',
/*
* Default fractal serializer.
*/
'serializer' => DataArraySerializer::class,
];
+27
View File
@@ -0,0 +1,27 @@
<?php
return [
/*
* DataTables JavaScript global namespace.
*/
'namespace' => 'LaravelDataTables',
/*
* Default table attributes when generating the table.
*/
'table' => [
'class' => 'table',
'id' => 'dataTableBuilder',
],
/*
* Html builder script template.
*/
'script' => 'datatables::script',
/*
* Html builder script template for DataTables Editor integration.
*/
'editor' => 'datatables::editor',
];
+135
View File
@@ -0,0 +1,135 @@
<?php
use Yajra\DataTables\ApiResourceDataTable;
use Yajra\DataTables\CollectionDataTable;
use Yajra\DataTables\EloquentDataTable;
use Yajra\DataTables\PaginatorDataTable;
use Yajra\DataTables\QueryDataTable;
return [
/*
* DataTables search options.
*/
'search' => [
/*
* Smart search will enclose search keyword with wildcard string "%keyword%".
* SQL: column LIKE "%keyword%"
*/
'smart' => true,
/*
* Multi-term search will explode search keyword using spaces resulting into multiple term search.
*/
'multi_term' => true,
/*
* Case insensitive will search the keyword in lower case format.
* SQL: LOWER(column) LIKE LOWER(keyword)
*/
'case_insensitive' => true,
/*
* Wild card will add "%" in between every characters of the keyword.
* SQL: column LIKE "%k%e%y%w%o%r%d%"
*/
'use_wildcards' => false,
/*
* Perform a search which starts with the given keyword.
* SQL: column LIKE "keyword%"
*/
'starts_with' => false,
],
/*
* DataTables internal index id response column name.
*/
'index_column' => 'DT_RowIndex',
/*
* List of available builders for DataTables.
* This is where you can register your custom DataTables builder.
*/
'engines' => [
'eloquent' => EloquentDataTable::class,
'query' => QueryDataTable::class,
'collection' => CollectionDataTable::class,
'paginator' => PaginatorDataTable::class,
'resource' => ApiResourceDataTable::class,
],
/*
* DataTables accepted builder to engine mapping.
* This is where you can override which engine a builder should use
* Note, only change this if you know what you are doing!
*/
'builders' => [
// Illuminate\Database\Eloquent\Relations\Relation::class => 'eloquent',
// Illuminate\Database\Eloquent\Builder::class => 'eloquent',
// Illuminate\Database\Query\Builder::class => 'query',
// Illuminate\Support\Collection::class => 'collection',
// Illuminate\Pagination\LengthAwarePaginator::class => 'paginator',
],
/*
* Nulls last sql pattern for PostgreSQL & Oracle.
* For MySQL, use 'CASE WHEN :column IS NULL THEN 1 ELSE 0 END, :column :direction'
*/
'nulls_last_sql' => ':column :direction NULLS LAST',
/*
* User friendly message to be displayed on user if error occurs.
* Possible values:
* null - The exception message will be used on error response.
* 'throw' - Throws a \Yajra\DataTables\Exceptions\Exception. Use your custom error handler if needed.
* 'custom message' - Any friendly message to be displayed to the user. You can also use translation key.
*/
'error' => env('DATATABLES_ERROR', null),
/*
* Default columns definition of DataTable utility functions.
*/
'columns' => [
/*
* List of columns hidden/removed on json response.
*/
'excess' => ['rn', 'row_num'],
/*
* List of columns to be escaped. If set to *, all columns are escape.
* Note: You can set the value to empty array to disable XSS protection.
*/
'escape' => '*',
/*
* List of columns that are allowed to display html content.
* Note: Adding columns to list will make us available to XSS attacks.
*/
'raw' => ['action'],
/*
* List of columns are forbidden from being searched/sorted.
*/
'blacklist' => ['password', 'remember_token'],
/*
* List of columns that are only allowed for search/sort.
* If set to *, all columns are allowed.
*/
'whitelist' => '*',
],
/*
* JsonResponse header and options config.
*/
'json' => [
'header' => [],
'options' => 0,
],
/*
* Default condition to determine if a parameter is a callback or not.
* Callbacks needs to start by those terms, or they will be cast to string.
*/
'callback' => ['$', '$.', 'function'],
];
+301
View File
@@ -0,0 +1,301 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Settings
|--------------------------------------------------------------------------
|
| Set some default values. It is possible to add all defines that can be set
| in dompdf_config.inc.php. You can also override the entire config file.
|
*/
'show_warnings' => false, // Throw an Exception on warnings from dompdf
'public_path' => null, // Override the public path if needed
/*
* Dejavu Sans font is missing glyphs for converted entities, turn it off if you need to show € and £.
*/
'convert_entities' => true,
'options' => [
/**
* The location of the DOMPDF font directory
*
* The location of the directory where DOMPDF will store fonts and font metrics
* Note: This directory must exist and be writable by the webserver process.
* *Please note the trailing slash.*
*
* Notes regarding fonts:
* Additional .afm font metrics can be added by executing load_font.php from command line.
*
* Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must
* be embedded in the pdf file or the PDF may not display correctly. This can significantly
* increase file size unless font subsetting is enabled. Before embedding a font please
* review your rights under the font license.
*
* Any font specification in the source HTML is translated to the closest font available
* in the font directory.
*
* The pdf standard "Base 14 fonts" are:
* Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique,
* Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique,
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
* Symbol, ZapfDingbats.
*/
'font_dir' => storage_path('fonts'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
/**
* The location of the DOMPDF font cache directory
*
* This directory contains the cached font metrics for the fonts used by DOMPDF.
* This directory can be the same as DOMPDF_FONT_DIR
*
* Note: This directory must exist and be writable by the webserver process.
*/
'font_cache' => storage_path('fonts'),
/**
* The location of a temporary directory.
*
* The directory specified must be writeable by the webserver process.
* The temporary directory is required to download remote images and when
* using the PDFLib back end.
*/
'temp_dir' => sys_get_temp_dir(),
/**
* ==== IMPORTANT ====
*
* dompdf's "chroot": Prevents dompdf from accessing system files or other
* files on the webserver. All local files opened by dompdf must be in a
* subdirectory of this directory. DO NOT set it to '/' since this could
* allow an attacker to use dompdf to read any files on the server. This
* should be an absolute path.
* This is only checked on command line call by dompdf.php, but not by
* direct class use like:
* $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output();
*/
'chroot' => realpath(base_path()),
/**
* Protocol whitelist
*
* Protocols and PHP wrappers allowed in URIs, and the validation rules
* that determine if a resouce may be loaded. Full support is not guaranteed
* for the protocols/wrappers specified
* by this array.
*
* @var array
*/
'allowed_protocols' => [
'data://' => ['rules' => []],
'file://' => ['rules' => []],
'http://' => ['rules' => []],
'https://' => ['rules' => []],
],
/**
* Operational artifact (log files, temporary files) path validation
*/
'artifactPathValidation' => null,
/**
* @var string
*/
'log_output_file' => null,
/**
* Whether to enable font subsetting or not.
*/
'enable_font_subsetting' => false,
/**
* The PDF rendering backend to use
*
* Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and
* 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will
* fall back on CPDF. 'GD' renders PDFs to graphic files.
* {@link * Canvas_Factory} ultimately determines which rendering class to
* instantiate based on this setting.
*
* Both PDFLib & CPDF rendering backends provide sufficient rendering
* capabilities for dompdf, however additional features (e.g. object,
* image and font support, etc.) differ between backends. Please see
* {@link PDFLib_Adapter} for more information on the PDFLib backend
* and {@link CPDF_Adapter} and lib/class.pdf.php for more information
* on CPDF. Also see the documentation for each backend at the links
* below.
*
* The GD rendering backend is a little different than PDFLib and
* CPDF. Several features of CPDF and PDFLib are not supported or do
* not make any sense when creating image files. For example,
* multiple pages are not supported, nor are PDF 'objects'. Have a
* look at {@link GD_Adapter} for more information. GD support is
* experimental, so use it at your own risk.
*
* @link http://www.pdflib.com
* @link http://www.ros.co.nz/pdf
* @link http://www.php.net/image
*/
'pdf_backend' => 'CPDF',
/**
* html target media view which should be rendered into pdf.
* List of types and parsing rules for future extensions:
* http://www.w3.org/TR/REC-html40/types.html
* screen, tty, tv, projection, handheld, print, braille, aural, all
* Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3.
* Note, even though the generated pdf file is intended for print output,
* the desired content might be different (e.g. screen or projection view of html file).
* Therefore allow specification of content here.
*/
'default_media_type' => 'screen',
/**
* The default paper size.
*
* North America standard is "letter"; other countries generally "a4"
*
* @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.)
*/
'default_paper_size' => 'a4',
/**
* The default paper orientation.
*
* The orientation of the page (portrait or landscape).
*
* @var string
*/
'default_paper_orientation' => 'portrait',
/**
* The default font family
*
* Used if no suitable fonts can be found. This must exist in the font folder.
*
* @var string
*/
'default_font' => 'serif',
/**
* Image DPI setting
*
* This setting determines the default DPI setting for images and fonts. The
* DPI may be overridden for inline images by explictly setting the
* image's width & height style attributes (i.e. if the image's native
* width is 600 pixels and you specify the image's width as 72 points,
* the image will have a DPI of 600 in the rendered PDF. The DPI of
* background images can not be overridden and is controlled entirely
* via this parameter.
*
* For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI).
* If a size in html is given as px (or without unit as image size),
* this tells the corresponding size in pt.
* This adjusts the relative sizes to be similar to the rendering of the
* html page in a reference browser.
*
* In pdf, always 1 pt = 1/72 inch
*
* Rendering resolution of various browsers in px per inch:
* Windows Firefox and Internet Explorer:
* SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:?
* Linux Firefox:
* about:config *resolution: Default:96
* (xorg screen dimension in mm and Desktop font dpi settings are ignored)
*
* Take care about extra font/image zoom factor of browser.
*
* In images, <img> size in pixel attribute, img css style, are overriding
* the real image dimension in px for rendering.
*
* @var int
*/
'dpi' => 96,
/**
* Enable embedded PHP
*
* If this setting is set to true then DOMPDF will automatically evaluate embedded PHP contained
* within <script type="text/php"> ... </script> tags.
*
* ==== IMPORTANT ==== Enabling this for documents you do not trust (e.g. arbitrary remote html pages)
* is a security risk.
* Embedded scripts are run with the same level of system access available to dompdf.
* Set this option to false (recommended) if you wish to process untrusted documents.
* This setting may increase the risk of system exploit.
* Do not change this settings without understanding the consequences.
* Additional documentation is available on the dompdf wiki at:
* https://github.com/dompdf/dompdf/wiki
*
* @var bool
*/
'enable_php' => false,
/**
* Enable inline JavaScript
*
* If this setting is set to true then DOMPDF will automatically insert JavaScript code contained
* within <script type="text/javascript"> ... </script> tags as written into the PDF.
* NOTE: This is PDF-based JavaScript to be executed by the PDF viewer,
* not browser-based JavaScript executed by Dompdf.
*
* @var bool
*/
'enable_javascript' => true,
/**
* Enable remote file access
*
* If this setting is set to true, DOMPDF will access remote sites for
* images and CSS files as required.
*
* ==== IMPORTANT ====
* This can be a security risk, in particular in combination with isPhpEnabled and
* allowing remote html code to be passed to $dompdf = new DOMPDF(); $dompdf->load_html(...);
* This allows anonymous users to download legally doubtful internet content which on
* tracing back appears to being downloaded by your server, or allows malicious php code
* in remote html pages to be executed by your server with your account privileges.
*
* This setting may increase the risk of system exploit. Do not change
* this settings without understanding the consequences. Additional
* documentation is available on the dompdf wiki at:
* https://github.com/dompdf/dompdf/wiki
*
* @var bool
*/
'enable_remote' => false,
/**
* List of allowed remote hosts
*
* Each value of the array must be a valid hostname.
*
* This will be used to filter which resources can be loaded in combination with
* isRemoteEnabled. If enable_remote is FALSE, then this will have no effect.
*
* Leave to NULL to allow any remote host.
*
* @var array|null
*/
'allowed_remote_hosts' => null,
/**
* A ratio applied to the fonts height to be more like browsers' line height
*/
'font_height_ratio' => 1.1,
/**
* Use the HTML5 Lib parser
*
* @deprecated This feature is now always on in dompdf 2.x
*
* @var bool
*/
'enable_html5_parser' => true,
],
];
+137
View File
@@ -0,0 +1,137 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Broadcasting
|--------------------------------------------------------------------------
|
| By uncommenting the Laravel Echo configuration, you may connect Filament
| to any Pusher-compatible websockets server.
|
| This will allow your users to receive real-time notifications.
|
*/
'broadcasting' => [
// 'echo' => [
// 'broadcaster' => 'pusher',
// 'key' => env('VITE_PUSHER_APP_KEY'),
// 'cluster' => env('VITE_PUSHER_APP_CLUSTER'),
// 'wsHost' => env('VITE_PUSHER_HOST'),
// 'wsPort' => env('VITE_PUSHER_PORT'),
// 'wssPort' => env('VITE_PUSHER_PORT'),
// 'authEndpoint' => '/broadcasting/auth',
// 'disableStats' => true,
// 'encrypted' => true,
// 'forceTLS' => env('VITE_PUSHER_SCHEME', 'https') === 'https',
// ],
],
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| This is the storage disk Filament will use to store files. You may use
| any of the disks defined in the `config/filesystems.php`.
|
*/
'default_filesystem_disk' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Temporary File URL Expiry
|--------------------------------------------------------------------------
|
| When Filament generates temporary URLs for previewing private files
| (file uploads, image columns, image entries, rich editor attachments,
| etc.), this value controls how many minutes those URLs remain valid.
|
| The generated URL's expiry is rounded up to the end of the hour it
| falls in, so the effective lifetime will be between this value and
| this value plus up to 60 minutes.
|
*/
'temporary_file_url_expiry_minutes' => 30,
/*
|--------------------------------------------------------------------------
| Assets Path
|--------------------------------------------------------------------------
|
| This is the directory where Filament's assets will be published to. It
| is relative to the `public` directory of your Laravel application.
|
| After changing the path, you should run `php artisan filament:assets`.
|
*/
'assets_path' => null,
/*
|--------------------------------------------------------------------------
| Cache Path
|--------------------------------------------------------------------------
|
| This is the directory that Filament will use to store cache files that
| are used to optimize the registration of components.
|
| After changing the path, you should run `php artisan filament:cache-components`.
|
*/
'cache_path' => base_path('bootstrap/cache/filament'),
/*
|--------------------------------------------------------------------------
| Livewire Loading Delay
|--------------------------------------------------------------------------
|
| This sets the delay before loading indicators appear.
|
| Setting this to 'none' makes indicators appear immediately, which can be
| desirable for high-latency connections. Setting it to 'default' applies
| Livewire's standard 200ms delay.
|
*/
'livewire_loading_delay' => 'default',
/*
|--------------------------------------------------------------------------
| File Generation
|--------------------------------------------------------------------------
|
| Artisan commands that generate files can be configured here by setting
| configuration flags that will impact their location or content.
|
| Often, this is useful to preserve file generation behavior from a
| previous version of Filament, to ensure consistency between older and
| newer generated files. These flags are often documented in the upgrade
| guide for the version of Filament you are upgrading to.
|
*/
'file_generation' => [
'flags' => [],
],
/*
|--------------------------------------------------------------------------
| System Route Prefix
|--------------------------------------------------------------------------
|
| This is the prefix used for the system routes that Filament registers,
| such as the routes for downloading exports and failed import rows.
|
*/
'system_route_prefix' => 'filament',
];
+254
View File
@@ -0,0 +1,254 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Horizon Name
|--------------------------------------------------------------------------
|
| This name appears in notifications and in the Horizon UI. Unique names
| can be useful while running multiple instances of Horizon within an
| application, allowing you to identify the Horizon you're viewing.
|
*/
'name' => env('HORIZON_NAME'),
/*
|--------------------------------------------------------------------------
| Horizon Domain
|--------------------------------------------------------------------------
|
| This is the subdomain where Horizon will be accessible from. If this
| setting is null, Horizon will reside under the same domain as the
| application. Otherwise, this value will serve as the subdomain.
|
*/
'domain' => env('HORIZON_DOMAIN'),
/*
|--------------------------------------------------------------------------
| Horizon Path
|--------------------------------------------------------------------------
|
| This is the URI path where Horizon will be accessible from. Feel free
| to change this path to anything you like. Note that the URI will not
| affect the paths of its internal API that aren't exposed to users.
|
*/
'path' => env('HORIZON_PATH', 'horizon'),
/*
|--------------------------------------------------------------------------
| Horizon Redis Connection
|--------------------------------------------------------------------------
|
| This is the name of the Redis connection where Horizon will store the
| meta information required for it to function. It includes the list
| of supervisors, failed jobs, job metrics, and other information.
|
*/
'use' => 'default',
/*
|--------------------------------------------------------------------------
| Horizon Redis Prefix
|--------------------------------------------------------------------------
|
| This prefix will be used when storing all Horizon data in Redis. You
| may modify the prefix when you are running multiple installations
| of Horizon on the same server so that they don't have problems.
|
*/
'prefix' => env(
'HORIZON_PREFIX',
Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'
),
/*
|--------------------------------------------------------------------------
| Horizon Route Middleware
|--------------------------------------------------------------------------
|
| These middleware will get attached onto each Horizon route, giving you
| the chance to add your own middleware to this list or change any of
| the existing middleware. Or, you can simply stick with this list.
|
*/
'middleware' => ['web'],
/*
|--------------------------------------------------------------------------
| Queue Wait Time Thresholds
|--------------------------------------------------------------------------
|
| This option allows you to configure when the LongWaitDetected event
| will be fired. Every connection / queue combination may have its
| own, unique threshold (in seconds) before this event is fired.
|
*/
'waits' => [
'redis:default' => 60,
],
/*
|--------------------------------------------------------------------------
| Job Trimming Times
|--------------------------------------------------------------------------
|
| Here you can configure for how long (in minutes) you desire Horizon to
| persist the recent and failed jobs. Typically, recent jobs are kept
| for one hour while all failed jobs are stored for an entire week.
|
*/
'trim' => [
'recent' => 60,
'pending' => 60,
'completed' => 60,
'recent_failed' => 10080,
'failed' => 10080,
'monitored' => 10080,
],
/*
|--------------------------------------------------------------------------
| Silenced Jobs
|--------------------------------------------------------------------------
|
| Silencing a job will instruct Horizon to not place the job in the list
| of completed jobs within the Horizon dashboard. This setting may be
| used to fully remove any noisy jobs from the completed jobs list.
|
*/
'silenced' => [
// App\Jobs\ExampleJob::class,
],
'silenced_tags' => [
// 'notifications',
],
/*
|--------------------------------------------------------------------------
| Metrics
|--------------------------------------------------------------------------
|
| Here you can configure how many snapshots should be kept to display in
| the metrics graph. This will get used in combination with Horizon's
| `horizon:snapshot` schedule to define how long to retain metrics.
|
*/
'metrics' => [
'trim_snapshots' => [
'job' => 24,
'queue' => 24,
],
],
/*
|--------------------------------------------------------------------------
| Fast Termination
|--------------------------------------------------------------------------
|
| When this option is enabled, Horizon's "terminate" command will not
| wait on all of the workers to terminate unless the --wait option
| is provided. Fast termination can shorten deployment delay by
| allowing a new instance of Horizon to start while the last
| instance will continue to terminate each of its workers.
|
*/
'fast_termination' => false,
/*
|--------------------------------------------------------------------------
| Memory Limit (MB)
|--------------------------------------------------------------------------
|
| This value describes the maximum amount of memory the Horizon master
| supervisor may consume before it is terminated and restarted. For
| configuring these limits on your workers, see the next section.
|
*/
'memory_limit' => 64,
/*
|--------------------------------------------------------------------------
| Queue Worker Configuration
|--------------------------------------------------------------------------
|
| Here you may define the queue worker settings used by your application
| in all environments. These supervisors and settings handle all your
| queued jobs and will be provisioned by Horizon during deployment.
|
*/
'defaults' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'auto',
'autoScalingStrategy' => 'time',
'maxProcesses' => 1,
'maxTime' => 0,
'maxJobs' => 0,
'memory' => 128,
'tries' => 1,
'timeout' => 60,
'nice' => 0,
],
],
'environments' => [
'production' => [
'supervisor-1' => [
'maxProcesses' => 10,
'balanceMaxShift' => 1,
'balanceCooldown' => 3,
],
],
'local' => [
'supervisor-1' => [
'maxProcesses' => 3,
],
],
],
/*
|--------------------------------------------------------------------------
| File Watcher Configuration
|--------------------------------------------------------------------------
|
| The following list of directories and files will be watched when using
| the `horizon:listen` command. Whenever any directories or files are
| changed, Horizon will automatically restart to apply all changes.
|
*/
'watch' => [
'app',
'bootstrap',
'config/**/*.php',
'database/**/*.php',
'public/**/*.php',
'resources/**/*.php',
'routes',
'composer.lock',
'composer.json',
'.env',
],
];
+81
View File
@@ -0,0 +1,81 @@
<?php
use Laravel\Jetstream\Features;
use Laravel\Jetstream\Http\Middleware\AuthenticateSession;
return [
/*
|--------------------------------------------------------------------------
| Jetstream Stack
|--------------------------------------------------------------------------
|
| This configuration value informs Jetstream which "stack" you will be
| using for your application. In general, this value is set for you
| during installation and will not need to be changed after that.
|
*/
'stack' => 'inertia',
/*
|--------------------------------------------------------------------------
| Jetstream Route Middleware
|--------------------------------------------------------------------------
|
| Here you may specify which middleware Jetstream will assign to the routes
| that it registers with the application. When necessary, you may modify
| these middleware; however, this default value is usually sufficient.
|
*/
'middleware' => ['web'],
'auth_session' => AuthenticateSession::class,
/*
|--------------------------------------------------------------------------
| Jetstream Guard
|--------------------------------------------------------------------------
|
| Here you may specify the authentication guard Jetstream will use while
| authenticating users. This value should correspond with one of your
| guards that is already present in your "auth" configuration file.
|
*/
'guard' => 'sanctum',
/*
|--------------------------------------------------------------------------
| Features
|--------------------------------------------------------------------------
|
| Some of Jetstream's features are optional. You may disable the features
| by removing them from this array. You're free to only remove some of
| these features or you can even remove all of these if you need to.
|
*/
'features' => [
// Features::termsAndPrivacyPolicy(),
// Features::profilePhotos(),
// Features::api(),
// Features::teams(['invitations' => true]),
Features::accountDeletion(),
],
/*
|--------------------------------------------------------------------------
| Profile Photo Disk
|--------------------------------------------------------------------------
|
| This configuration value determines the default disk that will be used
| when storing profile photos for your application's users. Typically
| this will be the "public" disk but you may adjust this if needed.
|
*/
'profile_photo_disk' => 'public',
];
+282
View File
@@ -0,0 +1,282 @@
<?php
return [
/*
|---------------------------------------------------------------------------
| Component Locations
|---------------------------------------------------------------------------
|
| This value sets the root directories that'll be used to resolve view-based
| components like single and multi-file components. The make command will
| use the first directory in this array to add new component files to.
|
*/
'component_locations' => [
resource_path('views/components'),
resource_path('views/livewire'),
],
/*
|---------------------------------------------------------------------------
| Component Namespaces
|---------------------------------------------------------------------------
|
| This value sets default namespaces that will be used to resolve view-based
| components like single-file and multi-file components. These folders'll
| also be referenced when creating new components via the make command.
|
*/
'component_namespaces' => [
'layouts' => resource_path('views/layouts'),
'pages' => resource_path('views/pages'),
],
/*
|---------------------------------------------------------------------------
| Page Layout
|---------------------------------------------------------------------------
| The view that will be used as the layout when rendering a single component as
| an entire page via `Route::livewire('/post/create', 'pages::create-post')`.
| In this case, the content of pages::create-post will render into $slot.
|
*/
'component_layout' => 'layouts::app',
/*
|---------------------------------------------------------------------------
| Lazy Loading Placeholder
|---------------------------------------------------------------------------
| Livewire allows you to lazy load components that would otherwise slow down
| the initial page load. Every component can have a custom placeholder or
| you can define the default placeholder view for all components below.
|
*/
'component_placeholder' => null, // Example: 'placeholders::skeleton'
/*
|---------------------------------------------------------------------------
| Make Command
|---------------------------------------------------------------------------
| This value determines the default configuration for the artisan make command
| You can configure the component type (sfc, mfc, class) and whether to use
| the high-voltage (⚡) emoji as a prefix in the sfc|mfc component names.
|
*/
'make_command' => [
'type' => 'sfc', // Options: 'sfc', 'mfc', 'class'
'emoji' => true, // Options: true, false
'with' => [
'js' => false,
'css' => false,
'test' => false,
],
],
/*
|---------------------------------------------------------------------------
| Class Namespace
|---------------------------------------------------------------------------
|
| This value sets the root class namespace for Livewire component classes in
| your application. This value will change where component auto-discovery
| finds components. It's also referenced by the file creation commands.
|
*/
'class_namespace' => 'App\\Livewire',
/*
|---------------------------------------------------------------------------
| Class Path
|---------------------------------------------------------------------------
|
| This value is used to specify the path where Livewire component class files
| are created when running creation commands like `artisan make:livewire`.
| This path is customizable to match your projects directory structure.
|
*/
'class_path' => app_path('Livewire'),
/*
|---------------------------------------------------------------------------
| View Path
|---------------------------------------------------------------------------
|
| This value is used to specify where Livewire component Blade templates are
| stored when running file creation commands like `artisan make:livewire`.
| It is also used if you choose to omit a component's render() method.
|
*/
'view_path' => resource_path('views/livewire'),
/*
|---------------------------------------------------------------------------
| Temporary File Uploads
|---------------------------------------------------------------------------
|
| Livewire handles file uploads by storing uploads in a temporary directory
| before the file is stored permanently. All file uploads are directed to
| a global endpoint for temporary storage. You may configure this below:
|
*/
'temporary_file_upload' => [
'disk' => env('LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK'), // Example: 'local', 's3' | Default: 'default'
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
],
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
],
/*
|---------------------------------------------------------------------------
| Render On Redirect
|---------------------------------------------------------------------------
|
| This value determines if Livewire will run a component's `render()` method
| after a redirect has been triggered using something like `redirect(...)`
| Setting this to true will render the view once more before redirecting
|
*/
'render_on_redirect' => false,
/*
|---------------------------------------------------------------------------
| Eloquent Model Binding
|---------------------------------------------------------------------------
|
| Previous versions of Livewire supported binding directly to eloquent model
| properties using wire:model by default. However, this behavior has been
| deemed too "magical" and has therefore been put under a feature flag.
|
*/
'legacy_model_binding' => false,
/*
|---------------------------------------------------------------------------
| Auto-inject Frontend Assets
|---------------------------------------------------------------------------
|
| By default, Livewire automatically injects its JavaScript and CSS into the
| <head> and <body> of pages containing Livewire components. By disabling
| this behavior, you need to use @livewireStyles and @livewireScripts.
|
*/
'inject_assets' => true,
/*
|---------------------------------------------------------------------------
| Navigate (SPA mode)
|---------------------------------------------------------------------------
|
| By adding `wire:navigate` to links in your Livewire application, Livewire
| will prevent the default link handling and instead request those pages
| via AJAX, creating an SPA-like effect. Configure this behavior here.
|
*/
'navigate' => [
'show_progress_bar' => true,
'progress_bar_color' => '#2299dd',
],
/*
|---------------------------------------------------------------------------
| HTML Morph Markers
|---------------------------------------------------------------------------
|
| Livewire intelligently "morphs" existing HTML into the newly rendered HTML
| after each update. To make this process more reliable, Livewire injects
| "markers" into the rendered Blade surrounding @if, @class & @foreach.
|
*/
'inject_morph_markers' => true,
/*
|---------------------------------------------------------------------------
| Smart Wire Keys
|---------------------------------------------------------------------------
|
| Livewire uses loops and keys used within loops to generate smart keys that
| are applied to nested components that don't have them. This makes using
| nested components more reliable by ensuring that they all have keys.
|
*/
'smart_wire_keys' => true,
/*
|---------------------------------------------------------------------------
| Pagination Theme
|---------------------------------------------------------------------------
|
| When enabling Livewire's pagination feature by using the `WithPagination`
| trait, Livewire will use Tailwind templates to render pagination views
| on the page. If you want Bootstrap CSS, you can specify: "bootstrap"
|
*/
'pagination_theme' => 'tailwind',
/*
|---------------------------------------------------------------------------
| Release Token
|---------------------------------------------------------------------------
|
| This token is stored client-side and sent along with each request to check
| a users session to see if a new release has invalidated it. If there is
| a mismatch it will throw an error and prompt for a browser refresh.
|
*/
'release_token' => 'a',
/*
|---------------------------------------------------------------------------
| CSP Safe
|---------------------------------------------------------------------------
|
| This config is used to determine if Livewire will use the CSP-safe version
| of Alpine in its bundle. This is useful for applications that are using
| strict Content Security Policy (CSP) to protect against XSS attacks.
|
*/
'csp_safe' => false,
/*
|---------------------------------------------------------------------------
| Payload Guards
|---------------------------------------------------------------------------
|
| These settings protect against malicious or oversized payloads that could
| cause denial of service. The default values should feel reasonable for
| most web applications. Each can be set to null to disable the limit.
|
*/
'payload' => [
'max_size' => 1024 * 1024, // 1MB - maximum request payload size in bytes
'max_nesting_depth' => 10, // Maximum depth of dot-notation property paths
'max_calls' => 50, // Maximum method calls per request
'max_components' => 200, // Maximum components per batch request
],
];
+279
View File
@@ -0,0 +1,279 @@
<?php
declare(strict_types=1);
use LaravelLang\Config\Constants\RouteName;
use LaravelLang\LocaleList\Locale;
return [
/*
* Determines what type of files to use when updating language files.
*
* @see https://laravel-lang.com/configuration.html#inline
*
* By default, `false`.
*/
'inline' => (bool) env('LOCALIZATION_INLINE', env('LANG_PUBLISHER_INLINE')),
/*
* Do arrays need to be aligned by keys before processing arrays?
*
* @see https://laravel-lang.com/configuration.html#alignment
*
* By default, true
*/
'align' => (bool) env('LOCALIZATION_ALIGN', env('LANG_PUBLISHER_ALIGN', true)),
/*
* The language codes chosen for the files in this repository may not
* match the preferences for your project.
*
* Specify here mappings of localizations with your project.
*
* @see https://laravel-lang.com/configuration.html#aliases
*/
'aliases' => [
// \LaravelLang\LocaleList\Locale::German->value => 'de-DE',
// \LaravelLang\LocaleList\Locale::GermanSwitzerland->value => 'de-CH',
],
/*
* This option determines the mechanism for converting translation
* keys into a typographic version.
*
* @see https://laravel-lang.com/configuration.html#smart_punctuation
*
* By default, false
*/
'smart_punctuation' => [
'enable' => (bool) env('LOCALIZATION_SMART_ENABLED', false),
'common' => [
'double_quote_opener' => '“',
'double_quote_closer' => '”',
'single_quote_opener' => '‘',
'single_quote_closer' => '’',
],
'locales' => [
Locale::French->value => [
'double_quote_opener' => '«&nbsp;',
'double_quote_closer' => '&nbsp;»',
'single_quote_opener' => '‘',
'single_quote_closer' => '’',
],
Locale::Russian->value => [
'double_quote_opener' => '«',
'double_quote_closer' => '»',
'single_quote_opener' => '‘',
'single_quote_closer' => '’',
],
Locale::Ukrainian->value => [
'double_quote_opener' => '«',
'double_quote_closer' => '»',
'single_quote_opener' => '‘',
'single_quote_closer' => '’',
],
Locale::Belarusian->value => [
'double_quote_opener' => '«',
'double_quote_closer' => '»',
'single_quote_opener' => '‘',
'single_quote_closer' => '’',
],
],
],
/*
* This option defines the application's route settings.
*
* @see https://laravel-lang.com/configuration.html#routes
*/
'routes' => [
/*
* This option defines the settings for the key names used when working with application routing.
*
* Default values:
*
* parameter - locale
* header - Accept-Language
* cookie - Accept-Language
* session - Accept-Language
* column - column
*/
'names' => [
'parameter' => RouteName::Parameter,
'header' => RouteName::Header,
'cookie' => RouteName::Cookie,
'session' => RouteName::Session,
'column' => RouteName::Column,
],
/*
* This option specifies the prefix of route group names.
*
* By default, `localized.`
*/
'name_prefix' => env('LOCALIZATION_NAME_PREFIX', 'localized.'),
/*
* This option specifies the request redirection option when trying to open the default localization.
*
* Applies when using the `LaravelLang\Routes\Facades\LocalizationRoute` facade.
*/
'redirect_default' => (bool) env('LOCALIZATION_REDIRECT_DEFAULT', false),
/*
* This option defines the default localization, when used, the localization parameter will be removed from the URL.
*
* Applies when using the `localizedRoute` helper.
*/
'hide_default' => (bool) env('LOCALIZATION_HIDE_DEFAULT', false),
// This option contains settings for routes.
'group' => [
'middlewares' => [
// This option contains settings for routes without the prefix of the localization code.
'default' => [
LaravelLang\Routes\Middlewares\LocalizationByCookie::class,
LaravelLang\Routes\Middlewares\LocalizationByHeader::class,
LaravelLang\Routes\Middlewares\LocalizationBySession::class,
LaravelLang\Routes\Middlewares\LocalizationByModel::class,
],
// This option contains settings for routes with the prefix of the localization code.
'prefix' => [
LaravelLang\Routes\Middlewares\LocalizationByParameterPrefix::class,
LaravelLang\Routes\Middlewares\LocalizationByCookie::class,
LaravelLang\Routes\Middlewares\LocalizationByHeader::class,
LaravelLang\Routes\Middlewares\LocalizationBySession::class,
LaravelLang\Routes\Middlewares\LocalizationByModel::class,
],
],
],
],
/*
* This option defines settings for working with model translations.
*
* @see https://laravel-lang.com/configuration.html#models
*/
'models' => [
/*
* This option specifies a suffix for models containing translations.
*
* For example,
* main model is `App\Models\Page`
* translation model is `App\Models\PageTranslation`
*
* By default, `Translation`
*/
'suffix' => 'Translation',
/*
* This option determines the need to filter localizations loaded
* in the relay when using eager loading.
*
* By default, true.
*/
'filter' => [
'enabled' => (bool) env('LOCALIZATION_FILTER_ENABLED', true),
],
/*
* This option specifies a folder to store helper files for the IDE.
*
* By default, `vendor/_laravel_lang`
*/
'helpers' => env('VENDOR_PATH', base_path('vendor/_laravel_lang')),
],
/*
* This option contains a list of translators that the Laravel Lang Translator project works with.
*
* Google Translate is enabled by default.
*
* @see https://laravel-lang.com/configuration.html#translators
*/
'translators' => [
/*
* List of channels used for translations.
*
* By default,
*
* Google is enabled
* Deepl is disabled
* Yandex is disabled
*/
'channels' => [
'google' => [
'translator' => '\LaravelLang\Translator\Integrations\Google',
'enabled' => (bool) env('TRANSLATION_GOOGLE_ENABLED', true),
'priority' => (int) env('TRANSLATION_GOOGLE_PRIORITY', 1),
],
'deepl' => [
'translator' => '\LaravelLang\Translator\Integrations\Deepl',
'enabled' => (bool) env('TRANSLATION_DEEPL_ENABLED', false),
'priority' => (int) env('TRANSLATION_DEEPL_PRIORITY', 2),
'credentials' => [
'key' => (string) env('TRANSLATION_DEEPL_KEY'),
],
],
'yandex' => [
'translator' => '\LaravelLang\Translator\Integrations\Yandex',
'enabled' => (bool) env('TRANSLATION_YANDEX_ENABLED', false),
'priority' => (int) env('TRANSLATION_YANDEX_PRIORITY', 3),
'credentials' => [
'key' => (string) env('TRANSLATION_YANDEX_KEY'),
'folder' => (string) env('TRANSLATION_YANDEX_FOLDER_ID'),
],
],
],
'options' => [
/*
* Set a custom pattern for extracting replaceable keywords from the string,
* default to extracting words prefixed with a colon.
*
* Available options:
*
* `true` is a `/:(\w+)/`
* `false` will disable regular expression processing
* `/any regex/` - any regular expression you specify
*
* By default, `true`
*
* @example (e.g. "Hello :name" will extract "name")
*/
'preserve_parameters' => true,
],
],
];
+54
View File
@@ -0,0 +1,54 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Redirect Domains
|--------------------------------------------------------------------------
|
| These domains are the domains that OAuth clients are permitted to use
| for redirect URIs. Each domain should be specified with its scheme
| and host. Domains not in this list will raise validation errors.
|
| An "*" may be used to allow all domains.
|
*/
'redirect_domains' => [
'*',
// 'https://example.com',
// 'http://localhost',
],
/*
|--------------------------------------------------------------------------
| Allowed Custom Schemes
|--------------------------------------------------------------------------
|
| Native desktop OAuth clients like Cursor and VS Code use private-use URI
| schemes (RFC 8252) for redirect callbacks instead of standard schemes
| like HTTPS. Here, you may list which custom schemes you will allow.
|
*/
'custom_schemes' => [
// 'claude',
// 'cursor',
// 'vscode',
],
/*
|--------------------------------------------------------------------------
| Authorization Server
|--------------------------------------------------------------------------
|
| Here you may configure the OAuth authorization server issuer identifier
| per RFC 8414. This value appears in your protected resource and auth
| server metadata endpoints. When null, this defaults to `url('/')`.
|
*/
'authorization_server' => null,
];
+363
View File
@@ -0,0 +1,363 @@
<?php
use Spatie\ImageOptimizer\Optimizers\Avifenc;
use Spatie\ImageOptimizer\Optimizers\Cwebp;
use Spatie\ImageOptimizer\Optimizers\Gifsicle;
use Spatie\ImageOptimizer\Optimizers\Jpegoptim;
use Spatie\ImageOptimizer\Optimizers\Optipng;
use Spatie\ImageOptimizer\Optimizers\Pngquant;
use Spatie\ImageOptimizer\Optimizers\Svgo;
use Spatie\MediaLibrary\Conversions\ImageGenerators\Avif;
use Spatie\MediaLibrary\Conversions\ImageGenerators\Image;
use Spatie\MediaLibrary\Conversions\ImageGenerators\Pdf;
use Spatie\MediaLibrary\Conversions\ImageGenerators\Svg;
use Spatie\MediaLibrary\Conversions\ImageGenerators\Video;
use Spatie\MediaLibrary\Conversions\ImageGenerators\Webp;
use Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob;
use Spatie\MediaLibrary\Downloaders\DefaultDownloader;
use Spatie\MediaLibrary\MediaCollections\FileAdder;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\MediaLibrary\MediaCollections\Models\Observers\MediaObserver;
use Spatie\MediaLibrary\ResponsiveImages\Jobs\GenerateResponsiveImagesJob;
use Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator\Blurred;
use Spatie\MediaLibrary\ResponsiveImages\WidthCalculator\FileSizeOptimizedWidthCalculator;
use Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer;
use Spatie\MediaLibrary\Support\FileRemover\DefaultFileRemover;
use Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator;
use Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator;
use Spatie\MediaLibraryPro\Models\TemporaryUpload;
return [
/*
* The disk on which to store added files and derived images by default. Choose
* one or more of the disks you've configured in config/filesystems.php.
*/
'disk_name' => env('MEDIA_DISK', 'public'),
/*
* The disk on which to store conversions (thumbnails, etc.) and responsive images
* when no disk is specified explicitly on the media collection or via
* `storingConversionsOnDisk()`. When left null, conversions are stored on the
* same disk as the original media — preserving previous behavior.
*
* This is useful when the originals live on a remote disk (e.g. S3) but the
* generated derivatives should stay local for faster access and lower egress.
*/
'conversions_disk_name' => env('MEDIA_CONVERSIONS_DISK', null),
/*
* The maximum file size of an item in bytes.
* Adding a larger file will result in an exception.
*/
'max_file_size' => 1024 * 1024 * 10, // 10MB
/*
* Uploads whose file name contains any of these extensions will be rejected.
* The check looks at every extension in the file name, so a file named
* `malicious.php.jpg` is blocked as well. Matching is case-insensitive
* and a leading dot is optional.
*
* The default list lives on the `FileAdder` class so the shipped config
* and the in-code fallback (used when the config is cached without the
* key) cannot drift. Override here to extend or shrink it.
*/
'disallowed_extensions' => FileAdder::$defaultDisallowedExtensions,
/*
* When this is set to an array of extensions, only uploads whose final
* extension is in the list will be accepted. Matching is case-insensitive
* and a leading dot is optional. The `disallowed_extensions` list above
* is still enforced, so an interior dangerous segment (such as the `php`
* in `shell.php.jpg`) is rejected even if the final extension is allowed.
* Leave `null` to disable allowlisting.
*/
'allowed_extensions' => null,
/*
* This queue connection will be used to generate derived and responsive images.
* Leave empty to use the default queue connection.
*/
'queue_connection_name' => env('QUEUE_CONNECTION', 'sync'),
/*
* This queue will be used to generate derived and responsive images.
* Leave empty to use the default queue.
*/
'queue_name' => env('MEDIA_QUEUE', ''),
/*
* By default all conversions will be performed on a queue.
*/
'queue_conversions_by_default' => env('QUEUE_CONVERSIONS_BY_DEFAULT', true),
/*
* Should database transactions be run after database commits?
*/
'queue_conversions_after_database_commit' => env('QUEUE_CONVERSIONS_AFTER_DB_COMMIT', true),
/*
* The fully qualified class name of the media model.
*/
'media_model' => Media::class,
/*
* The fully qualified class name of the media observer.
*/
'media_observer' => MediaObserver::class,
/*
* When enabled, media collections will be serialised using the default
* laravel model serialization behaviour.
*
* Keep this option disabled if using Media Library Pro components (https://medialibrary.pro)
*/
'use_default_collection_serialization' => false,
/*
* The fully qualified class name of the model used for temporary uploads.
*
* This model is only used in Media Library Pro (https://medialibrary.pro)
*/
'temporary_upload_model' => TemporaryUpload::class,
/*
* When enabled, Media Library Pro will only process temporary uploads that were uploaded
* in the same session. You can opt to disable this for stateless usage of
* the pro components.
*/
'enable_temporary_uploads_session_affinity' => true,
/*
* When enabled, Media Library pro will generate thumbnails for uploaded file.
*/
'generate_thumbnails_for_temporary_uploads' => true,
/*
* This is the class that is responsible for naming generated files.
*/
'file_namer' => DefaultFileNamer::class,
/*
* The class that contains the strategy for determining a media file's path.
*/
'path_generator' => DefaultPathGenerator::class,
/*
* The class that contains the strategy for determining how to remove files.
*/
'file_remover_class' => DefaultFileRemover::class,
/*
* Here you can specify which path generator should be used for the given class.
*/
'custom_path_generators' => [
// Model::class => PathGenerator::class
// or
// 'model_morph_alias' => PathGenerator::class
],
/*
* When urls to files get generated, this class will be called. Use the default
* if your files are stored locally above the site root or on s3.
*/
'url_generator' => DefaultUrlGenerator::class,
/*
* Moves media on updating to keep path consistent. Enable it only with a custom
* PathGenerator that uses, for example, the media UUID.
*/
'moves_media_on_update' => false,
/*
* Whether to activate versioning when urls to files get generated.
* When activated, this attaches a ?v=xx query string to the URL.
*/
'version_urls' => false,
/*
* The media library will try to optimize all converted images by removing
* metadata and applying a little bit of compression. These are
* the optimizers that will be used by default.
*/
'image_optimizers' => [
Jpegoptim::class => [
'-m85', // set maximum quality to 85%
'--force', // ensure that progressive generation is always done also if a little bigger
'--strip-all', // this strips out all text information such as comments and EXIF data
'--all-progressive', // this will make sure the resulting image is a progressive one
],
Pngquant::class => [
'--force', // required parameter for this package
],
Optipng::class => [
'-i0', // this will result in a non-interlaced, progressive scanned image
'-o2', // this set the optimization level to two (multiple IDAT compression trials)
'-quiet', // required parameter for this package
],
Svgo::class => [
'--disable=cleanupIDs', // disabling because it is known to cause troubles
],
Gifsicle::class => [
'-b', // required parameter for this package
'-O3', // this produces the slowest but best results
],
Cwebp::class => [
'-m 6', // for the slowest compression method in order to get the best compression.
'-pass 10', // for maximizing the amount of analysis pass.
'-mt', // multithreading for some speed improvements.
'-q 90', // quality factor that brings the least noticeable changes.
],
Avifenc::class => [
'-a cq-level=23', // constant quality level, lower values mean better quality and greater file size (0-63).
'-j all', // number of jobs (worker threads, "all" uses all available cores).
'--min 0', // min quantizer for color (0-63).
'--max 63', // max quantizer for color (0-63).
'--minalpha 0', // min quantizer for alpha (0-63).
'--maxalpha 63', // max quantizer for alpha (0-63).
'-a end-usage=q', // rate control mode set to Constant Quality mode.
'-a tune=ssim', // SSIM as tune the encoder for distortion metric.
],
],
/*
* These generators will be used to create an image of media files.
*/
'image_generators' => [
Image::class,
Webp::class,
Avif::class,
Pdf::class,
Svg::class,
Video::class,
],
/*
* The path where to store temporary files while performing image conversions.
* If set to null, storage_path('media-library/temp') will be used.
*/
'temporary_directory_path' => null,
/*
* The engine that should perform the image conversions.
* Should be either `gd`, `imagick` or `vips`.
*/
'image_driver' => env('IMAGE_DRIVER', 'gd'),
/*
* FFMPEG & FFProbe binaries paths, only used if you try to generate video
* thumbnails and have installed the php-ffmpeg/php-ffmpeg composer
* dependency.
*/
'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'),
'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'),
/*
* The timeout (in seconds) that will be used when generating video
* thumbnails via FFMPEG.
*/
'ffmpeg_timeout' => env('FFMPEG_TIMEOUT', 900),
/*
* The number of threads that FFMPEG should use. 0 means that FFMPEG
* may decide itself.
*/
'ffmpeg_threads' => env('FFMPEG_THREADS', 0),
/*
* Here you can override the class names of the jobs used by this package. Make sure
* your custom jobs extend the ones provided by the package.
*/
'jobs' => [
'perform_conversions' => PerformConversionsJob::class,
'generate_responsive_images' => GenerateResponsiveImagesJob::class,
],
/*
* When using the addMediaFromUrl method you may want to replace the default downloader.
* This is particularly useful when the url of the image is behind a firewall and
* need to add additional flags, possibly using curl.
*/
'media_downloader' => DefaultDownloader::class,
/*
* When using the addMediaFromUrl method the SSL is verified by default.
* This is option disables SSL verification when downloading remote media.
* Please note that this is a security risk and should only be false in a local environment.
*/
'media_downloader_ssl' => env('MEDIA_DOWNLOADER_SSL', true),
/*
* The default lifetime in minutes for temporary urls.
* This is used when you call the `getLastTemporaryUrl` or `getLastTemporaryUrl` method on a media item.
*/
'temporary_url_default_lifetime' => env('MEDIA_TEMPORARY_URL_DEFAULT_LIFETIME', 5),
'remote' => [
/*
* Any extra headers that should be included when uploading media to
* a remote disk. Even though supported headers may vary between
* different drivers, a sensible default has been provided.
*
* Supported by S3: CacheControl, Expires, StorageClass,
* ServerSideEncryption, Metadata, ACL, ContentEncoding
*/
'extra_headers' => [
'CacheControl' => 'max-age=604800',
],
],
'responsive_images' => [
/*
* This class is responsible for calculating the target widths of the responsive
* images. By default we optimize for filesize and create variations that each are 30%
* smaller than the previous one. More info in the documentation.
*
* https://docs.spatie.be/laravel-medialibrary/v9/advanced-usage/generating-responsive-images
*/
'width_calculator' => FileSizeOptimizedWidthCalculator::class,
/*
* By default rendering media to a responsive image will add some javascript and a tiny placeholder.
* This ensures that the browser can already determine the correct layout.
* When disabled, no tiny placeholder is generated.
*/
'use_tiny_placeholders' => true,
/*
* This class will generate the tiny placeholder used for progressive image loading. By default
* the media library will use a tiny blurred jpg image.
*/
'tiny_placeholder_generator' => Blurred::class,
],
/*
* When enabling this option, a route will be registered that will enable
* the Media Library Pro Vue and React components to move uploaded files
* in a S3 bucket to their right place.
*/
'enable_vapor_uploads' => env('ENABLE_MEDIA_LIBRARY_VAPOR_UPLOADS', false),
/*
* When converting Media instances to response the media library will add
* a `loading` attribute to the `img` tag. Here you can set the default
* value of that attribute.
*
* Possible values: 'lazy', 'eager', 'auto' or null if you don't want to set any loading instruction.
*
* More info: https://css-tricks.com/native-lazy-loading/
*/
'default_loading_attribute_value' => null,
/*
* You can specify a prefix for that is used for storing all media.
* If you set this to `/my-subdir`, all your media will be stored in a `/my-subdir` directory.
*/
'prefix' => env('MEDIA_PREFIX', ''),
/*
* When forcing lazy loading, media will be loaded even if you don't eager load media and you have
* disabled lazy loading globally in the service provider.
*/
'force_lazy_loading' => env('FORCE_MEDIA_LIBRARY_LAZY_LOADING', true),
];
+18
View File
@@ -0,0 +1,18 @@
<?php
return [
'namespace' => 'Modules',
'paths' => [
'modules' => base_path('Modules'),
'assets' => public_path('modules'),
'migration' => base_path('database/migrations'),
],
'scan' => [
'enabled' => true,
'paths' => [base_path('Modules')],
],
'composer' => [
'vendor' => 'mpba',
'author' => ['name' => 'MPBA Limited', 'email' => 'mark.gregory@mpba.co.uk'],
],
];
+224
View File
@@ -0,0 +1,224 @@
<?php
use Laravel\Octane\Contracts\OperationTerminated;
use Laravel\Octane\Events\RequestHandled;
use Laravel\Octane\Events\RequestReceived;
use Laravel\Octane\Events\RequestTerminated;
use Laravel\Octane\Events\TaskReceived;
use Laravel\Octane\Events\TaskTerminated;
use Laravel\Octane\Events\TickReceived;
use Laravel\Octane\Events\TickTerminated;
use Laravel\Octane\Events\WorkerErrorOccurred;
use Laravel\Octane\Events\WorkerStarting;
use Laravel\Octane\Events\WorkerStopping;
use Laravel\Octane\Listeners\CloseMonologHandlers;
use Laravel\Octane\Listeners\CollectGarbage;
use Laravel\Octane\Listeners\DisconnectFromDatabases;
use Laravel\Octane\Listeners\EnsureUploadedFilesAreValid;
use Laravel\Octane\Listeners\EnsureUploadedFilesCanBeMoved;
use Laravel\Octane\Listeners\FlushOnce;
use Laravel\Octane\Listeners\FlushTemporaryContainerInstances;
use Laravel\Octane\Listeners\FlushUploadedFiles;
use Laravel\Octane\Listeners\ReportException;
use Laravel\Octane\Listeners\StopWorkerIfNecessary;
use Laravel\Octane\Octane;
return [
/*
|--------------------------------------------------------------------------
| Octane Server
|--------------------------------------------------------------------------
|
| This value determines the default "server" that will be used by Octane
| when starting, restarting, or stopping your server via the CLI. You
| are free to change this to the supported server of your choosing.
|
| Supported: "roadrunner", "swoole", "frankenphp"
|
*/
'server' => env('OCTANE_SERVER', 'roadrunner'),
/*
|--------------------------------------------------------------------------
| Force HTTPS
|--------------------------------------------------------------------------
|
| When this configuration value is set to "true", Octane will inform the
| framework that all absolute links must be generated using the HTTPS
| protocol. Otherwise your links may be generated using plain HTTP.
|
*/
'https' => env('OCTANE_HTTPS', false),
/*
|--------------------------------------------------------------------------
| Octane Listeners
|--------------------------------------------------------------------------
|
| All of the event listeners for Octane's events are defined below. These
| listeners are responsible for resetting your application's state for
| the next request. You may even add your own listeners to the list.
|
*/
'listeners' => [
WorkerStarting::class => [
EnsureUploadedFilesAreValid::class,
EnsureUploadedFilesCanBeMoved::class,
],
RequestReceived::class => [
...Octane::prepareApplicationForNextOperation(),
...Octane::prepareApplicationForNextRequest(),
//
],
RequestHandled::class => [
//
],
RequestTerminated::class => [
// FlushUploadedFiles::class,
],
TaskReceived::class => [
...Octane::prepareApplicationForNextOperation(),
//
],
TaskTerminated::class => [
//
],
TickReceived::class => [
...Octane::prepareApplicationForNextOperation(),
//
],
TickTerminated::class => [
//
],
OperationTerminated::class => [
FlushOnce::class,
FlushTemporaryContainerInstances::class,
// DisconnectFromDatabases::class,
// CollectGarbage::class,
],
WorkerErrorOccurred::class => [
ReportException::class,
StopWorkerIfNecessary::class,
],
WorkerStopping::class => [
CloseMonologHandlers::class,
],
],
/*
|--------------------------------------------------------------------------
| Warm / Flush Bindings
|--------------------------------------------------------------------------
|
| The bindings listed below will either be pre-warmed when a worker boots
| or they will be flushed before every new request. Flushing a binding
| will force the container to resolve that binding again when asked.
|
*/
'warm' => [
...Octane::defaultServicesToWarm(),
],
'flush' => [
//
],
/*
|--------------------------------------------------------------------------
| Octane Swoole Tables
|--------------------------------------------------------------------------
|
| While using Swoole, you may define additional tables as required by the
| application. These tables can be used to store data that needs to be
| quickly accessed by other workers on the particular Swoole server.
|
*/
'tables' => [
'example:1000' => [
'name' => 'string:1000',
'votes' => 'int',
],
],
/*
|--------------------------------------------------------------------------
| Octane Swoole Cache Table
|--------------------------------------------------------------------------
|
| While using Swoole, you may leverage the Octane cache, which is powered
| by a Swoole table. You may set the maximum number of rows as well as
| the number of bytes per row using the configuration options below.
|
*/
'cache' => [
'rows' => 1000,
'bytes' => 10000,
],
/*
|--------------------------------------------------------------------------
| File Watching
|--------------------------------------------------------------------------
|
| The following list of files and directories will be watched when using
| the --watch option offered by Octane. If any of the directories and
| files are changed, Octane will automatically reload your workers.
|
*/
'watch' => [
'app',
'bootstrap',
'config/**/*.php',
'database/**/*.php',
'public/**/*.php',
'resources/**/*.php',
'routes',
'composer.lock',
'.env',
],
/*
|--------------------------------------------------------------------------
| Garbage Collection Threshold
|--------------------------------------------------------------------------
|
| When executing long-lived PHP scripts such as Octane, memory can build
| up before being cleared by PHP. You can force Octane to run garbage
| collection if your application consumes this amount of megabytes.
|
*/
'garbage' => 50,
/*
|--------------------------------------------------------------------------
| Maximum Execution Time
|--------------------------------------------------------------------------
|
| The following setting configures the maximum execution time for requests
| being handled by Octane. You may set this value to 0 to indicate that
| there isn't a specific time limit on Octane request execution time.
|
*/
'max_execution_time' => 30,
];
+117
View File
@@ -0,0 +1,117 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Relying Party ID
|--------------------------------------------------------------------------
|
| The relying party ID represents your application in the WebAuthn protocol.
| This is typically your domain (e.g., "example.com"). Passkeys are bound
| to this ID and can only be verified on matching domains.
|
*/
'relying_party_id' => parse_url(config('app.url'), PHP_URL_HOST),
/*
|--------------------------------------------------------------------------
| Allowed Origins
|--------------------------------------------------------------------------
|
| The origins permitted to complete WebAuthn ceremonies. Passkeys bound
| to the relying party ID above will only verify when the browser
| reports one of these origins. Defaults to your application URL.
|
*/
'allowed_origins' => [
config('app.url'),
],
/*
|--------------------------------------------------------------------------
| User Handle Secret
|--------------------------------------------------------------------------
|
| Secret used to derive a stable WebAuthn user handle from each user model.
| Set this explicitly if you rotate your application key.
|
*/
'user_handle_secret' => env('PASSKEYS_USER_HANDLE_SECRET', config('app.key')),
/*
|--------------------------------------------------------------------------
| WebAuthn Timeout
|--------------------------------------------------------------------------
|
| The timeout in milliseconds for WebAuthn operations. This determines
| how long users have to complete passkey registration or verification.
|
*/
'timeout' => 60000,
/*
|--------------------------------------------------------------------------
| Authentication Guard
|--------------------------------------------------------------------------
|
| The authentication guard to use when logging in users with passkeys.
| This should match your application's primary authentication guard.
|
*/
'guard' => 'web',
/*
|--------------------------------------------------------------------------
| Passkeys Routes Middleware
|--------------------------------------------------------------------------
|
| Here you may specify which middleware Passkeys will assign to the routes
| that it registers with the application. If necessary, you may change
| these middleware but typically this provided default is preferred.
|
*/
'middleware' => ['web'],
/*
|--------------------------------------------------------------------------
| Passkeys Management Middleware
|--------------------------------------------------------------------------
|
| Here you may specify the middleware applied to passkey management routes
| that create or delete passkeys. By default, Laravel's password
| confirmation middleware is used.
|
*/
'management_middleware' => ['password.confirm'],
/*
|--------------------------------------------------------------------------
| Passkeys Throttling
|--------------------------------------------------------------------------
|
| Middleware used to throttle passkey endpoints. Set to null to disable.
|
*/
'throttle' => 'throttle:6,1',
/*
|--------------------------------------------------------------------------
| Redirect
|--------------------------------------------------------------------------
|
| The path to redirect to after successful passkey verification.
|
*/
'redirect' => '/',
];
+219
View File
@@ -0,0 +1,219 @@
<?php
use Spatie\Permission\DefaultTeamResolver;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
return [
'models' => [
/*
* When using the "HasPermissions" trait from this package, we need to know which
* Eloquent model should be used to retrieve your permissions. Of course, it
* is often just the "Permission" model but you may use whatever you like.
*
* The model you want to use as a Permission model needs to implement the
* `Spatie\Permission\Contracts\Permission` contract.
*/
'permission' => Permission::class,
/*
* When using the "HasRoles" trait from this package, we need to know which
* Eloquent model should be used to retrieve your roles. Of course, it
* is often just the "Role" model but you may use whatever you like.
*
* The model you want to use as a Role model needs to implement the
* `Spatie\Permission\Contracts\Role` contract.
*/
'role' => Role::class,
/*
* When using the "Teams" feature from this package, we need to know which
* Eloquent model should be used to retrieve your teams. Of course, it
* is often just the "Team" model but you may use whatever you like.
*/
'team' => null,
/*
* When using the "HasModels" trait and passing raw IDs to syncModels,
* attachModels, or detachModels, this model class will be used to
* resolve those IDs. If null, defaults to the guard's model.
*/
'default_model' => null,
],
'table_names' => [
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'roles' => 'roles',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your permissions. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'permissions' => 'permissions',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your models permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_permissions' => 'model_has_permissions',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your models roles. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_roles' => 'model_has_roles',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'role_has_permissions' => 'role_has_permissions',
],
'column_names' => [
/*
* Change this if you want to name the related pivots other than defaults
*/
'role_pivot_key' => null, // default 'role_id',
'permission_pivot_key' => null, // default 'permission_id',
/*
* Change this if you want to name the related model primary key other than
* `model_id`.
*
* For example, this would be nice if your primary keys are all UUIDs. In
* that case, name this `model_uuid`.
*/
'model_morph_key' => 'model_id',
/*
* Change this if you want to use the teams feature and your related model's
* foreign key is other than `team_id`.
*/
'team_foreign_key' => 'team_id',
],
/*
* When set to true, the method for checking permissions will be registered on the gate.
* Set this to false if you want to implement custom logic for checking permissions.
*/
'register_permission_check_method' => true,
/*
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
*/
'register_octane_reset_listener' => false,
/*
* Events will fire when a role or permission is assigned/unassigned:
* \Spatie\Permission\Events\RoleAttachedEvent
* \Spatie\Permission\Events\RoleDetachedEvent
* \Spatie\Permission\Events\PermissionAttachedEvent
* \Spatie\Permission\Events\PermissionDetachedEvent
*
* To enable, set to true, and then create listeners to watch these events.
*/
'events_enabled' => false,
/*
* Teams Feature.
* When set to true the package implements teams using the 'team_foreign_key'.
* If you want the migrations to register the 'team_foreign_key', you must
* set this to true before doing the migration.
* If you already did the migration then you must make a new migration to also
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
* (view the latest version of this package's migration file)
*/
'teams' => false,
/*
* The class to use to resolve the permissions team id
*/
'team_resolver' => DefaultTeamResolver::class,
/*
* Passport Client Credentials Grant
* When set to true the package will use Passports Client to check permissions
*/
'use_passport_client_credentials' => false,
/*
* When set to true, the required permission names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_permission_in_exception' => false,
/*
* When set to true, the required role names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_role_in_exception' => false,
/*
* By default wildcard permission lookups are disabled.
* See documentation to understand supported syntax.
*/
'enable_wildcard_permission' => false,
/*
* The class to use for interpreting wildcard permissions.
* If you need to modify delimiters, override the class and specify its name here.
*/
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
/* Cache-specific settings */
'cache' => [
/*
* By default all permissions are cached for 24 hours to speed up performance.
* When permissions or roles are updated the cache is flushed automatically.
*/
'expiration_time' => DateInterval::createFromDateString('24 hours'),
/*
* The cache key used to store all permissions.
*/
'key' => 'spatie.permission.cache',
/*
* You may optionally indicate a specific cache driver to use for permission and
* role caching using any of the `store` drivers listed in the cache.php config
* file. Using 'default' here means to use the `default` set in cache.php.
*/
'store' => 'default',
],
];
+200
View File
@@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
use Stancl\Tenancy\Database\Models\Domain;
use Stancl\Tenancy\Database\Models\Tenant;
return [
'tenant_model' => Tenant::class,
'id_generator' => Stancl\Tenancy\UUIDGenerator::class,
'domain_model' => Domain::class,
/**
* The list of domains hosting your central app.
*
* Only relevant if you're using the domain or subdomain identification middleware.
*/
'central_domains' => [
'127.0.0.1',
'localhost',
],
/**
* Tenancy bootstrappers are executed when tenancy is initialized.
* Their responsibility is making Laravel features tenant-aware.
*
* To configure their behavior, see the config keys below.
*/
'bootstrappers' => [
Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
],
/**
* Database tenancy config. Used by DatabaseTenancyBootstrapper.
*/
'database' => [
'central_connection' => env('DB_CONNECTION', 'central'),
/**
* Connection used as a "template" for the dynamically created tenant database connection.
* Note: don't name your template connection tenant. That name is reserved by package.
*/
'template_tenant_connection' => null,
/**
* Tenant database names are created like this:
* prefix + tenant_id + suffix.
*/
'prefix' => 'tenant',
'suffix' => '',
/**
* TenantDatabaseManagers are classes that handle the creation & deletion of tenant databases.
*/
'managers' => [
'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class,
'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class,
'mariadb' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class,
'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class,
/**
* Use this database manager for MySQL to have a DB user created for each tenant database.
* You can customize the grants given to these users by changing the $grants property.
*/
// 'mysql' => Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager::class,
/**
* Disable the pgsql manager above, and enable the one below if you
* want to separate tenant DBs by schemas rather than databases.
*/
// 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database
],
],
/**
* Cache tenancy config. Used by CacheTenancyBootstrapper.
*
* This works for all Cache facade calls, cache() helper
* calls and direct calls to injected cache stores.
*
* Each key in cache will have a tag applied on it. This tag is used to
* scope the cache both when writing to it and when reading from it.
*
* You can clear cache selectively by specifying the tag.
*/
'cache' => [
'tag_base' => 'tenant', // This tag_base, followed by the tenant_id, will form a tag that will be applied on each cache call.
],
/**
* Filesystem tenancy config. Used by FilesystemTenancyBootstrapper.
* https://tenancyforlaravel.com/docs/v3/tenancy-bootstrappers/#filesystem-tenancy-boostrapper.
*/
'filesystem' => [
/**
* Each disk listed in the 'disks' array will be suffixed by the suffix_base, followed by the tenant_id.
*/
'suffix_base' => 'tenant',
'disks' => [
'local',
'public',
// 's3',
],
/**
* Use this for local disks.
*
* See https://tenancyforlaravel.com/docs/v3/tenancy-bootstrappers/#filesystem-tenancy-boostrapper
*/
'root_override' => [
// Disks whose roots should be overridden after storage_path() is suffixed.
'local' => '%storage_path%/app/',
'public' => '%storage_path%/app/public/',
],
/**
* Should storage_path() be suffixed.
*
* Note: Disabling this will likely break local disk tenancy. Only disable this if you're using an external file storage service like S3.
*
* For the vast majority of applications, this feature should be enabled. But in some
* edge cases, it can cause issues (like using Passport with Vapor - see #196), so
* you may want to disable this if you are experiencing these edge case issues.
*/
'suffix_storage_path' => true,
/**
* By default, asset() calls are made multi-tenant too. You can use global_asset() and mix()
* for global, non-tenant-specific assets. However, you might have some issues when using
* packages that use asset() calls inside the tenant app. To avoid such issues, you can
* disable asset() helper tenancy and explicitly use tenant_asset() calls in places
* where you want to use tenant-specific assets (product images, avatars, etc).
*/
'asset_helper_tenancy' => true,
],
/**
* Redis tenancy config. Used by RedisTenancyBootstrapper.
*
* Note: You need phpredis to use Redis tenancy.
*
* Note: You don't need to use this if you're using Redis only for cache.
* Redis tenancy is only relevant if you're making direct Redis calls,
* either using the Redis facade or by injecting it as a dependency.
*/
'redis' => [
'prefix_base' => 'tenant', // Each key in Redis will be prepended by this prefix_base, followed by the tenant id.
'prefixed_connections' => [ // Redis connections whose keys are prefixed, to separate one tenant's keys from another.
// 'default',
],
],
/**
* Features are classes that provide additional functionality
* not needed for tenancy to be bootstrapped. They are run
* regardless of whether tenancy has been initialized.
*
* See the documentation page for each class to
* understand which ones you want to enable.
*/
'features' => [
// Stancl\Tenancy\Features\UserImpersonation::class,
// Stancl\Tenancy\Features\TelescopeTags::class,
// Stancl\Tenancy\Features\UniversalRoutes::class,
// Stancl\Tenancy\Features\TenantConfig::class, // https://tenancyforlaravel.com/docs/v3/features/tenant-config
// Stancl\Tenancy\Features\CrossDomainRedirect::class, // https://tenancyforlaravel.com/docs/v3/features/cross-domain-redirect
// Stancl\Tenancy\Features\ViteBundler::class,
],
/**
* Should tenancy routes be registered.
*
* Tenancy routes include tenant asset routes. By default, this route is
* enabled. But it may be useful to disable them if you use external
* storage (e.g. S3 / Dropbox) or have a custom asset controller.
*/
'routes' => true,
/**
* Parameters used by the tenants:migrate command.
*/
'migration_parameters' => [
'--force' => true, // This needs to be true to run migrations in production.
'--path' => [database_path('migrations/tenant')],
'--realpath' => true,
],
/**
* Parameters used by the tenants:seed command.
*/
'seeder_parameters' => [
'--class' => 'DatabaseSeeder', // root seeder class
// '--force' => true, // This needs to be true to seed tenant databases in production
],
];
+63
View File
@@ -0,0 +1,63 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Console Commands
|--------------------------------------------------------------------------
|
| This option allows you to add additional Artisan commands that should
| be available within the Tinker environment. Once the command is in
| this array you may execute the command in Tinker using its name.
|
*/
'commands' => [
// App\Console\Commands\ExampleCommand::class,
],
/*
|--------------------------------------------------------------------------
| Auto Aliased Classes
|--------------------------------------------------------------------------
|
| Tinker will not automatically alias classes in your vendor namespaces
| but you may explicitly allow a subset of classes to get aliased by
| adding the names of each of those classes to the following list.
|
*/
'alias' => [
//
],
/*
|--------------------------------------------------------------------------
| Classes That Should Not Be Aliased
|--------------------------------------------------------------------------
|
| Typically, Tinker automatically aliases classes as you require them in
| Tinker. However, you may wish to never alias certain classes, which
| you may accomplish by listing the classes in the following array.
|
*/
'dont_alias' => [
'App\Nova',
],
/*
|--------------------------------------------------------------------------
| Project Trust Mode
|--------------------------------------------------------------------------
|
| PsySH restricts local project features unless your project is trusted.
| Set this to "always" to avoid untrusted project warnings in Tinker.
| Accepted values: "prompt", "always", "never", true, false, null.
|
*/
'trust_project' => env('TINKER_TRUST_PROJECT', 'always'),
];
+61
View File
@@ -6,6 +6,8 @@ use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Modules\Platform\app\Models\Company;
use Modules\Platform\app\Models\LicenseKey;
/**
* @extends Factory<User>
@@ -25,6 +27,7 @@ class UserFactory extends Factory
public function definition(): array
{
return [
'company_id' => fn (): int => $this->licensedCompany()->id,
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
@@ -33,6 +36,9 @@ class UserFactory extends Factory
'two_factor_secret' => null,
'two_factor_recovery_codes' => null,
'two_factor_confirmed_at' => null,
'is_platform_admin' => false,
'is_company_admin' => true,
'status' => 'active',
];
}
@@ -57,4 +63,59 @@ class UserFactory extends Factory
'two_factor_confirmed_at' => now(),
]);
}
/**
* Use the MPBA system tenant and platform administrator flags.
*/
public function mpbaPlatformAdmin(): static
{
return $this->state(fn (array $attributes) => [
'company_id' => $this->systemCompany()->id,
'is_platform_admin' => true,
'is_company_admin' => true,
'status' => 'active',
]);
}
/**
* Create or reuse a licensed tenant company for ordinary user tests.
*/
private function licensedCompany(): Company
{
$company = Company::query()->where('slug', 'test-company')->first();
if (! $company) {
$company = Company::query()->create([
'name' => 'Test Company',
'slug' => 'test-company',
'status' => 'active',
'timezone' => 'Europe/London',
'is_system_company' => false,
'billing_email' => 'billing@test-company.example',
]);
}
if (! $company->hasActiveLicense()) {
LicenseKey::issueFor($company, 'Automated Test Licence', 'enterprise', 250);
}
return $company;
}
/**
* Create or reuse the MPBA system tenant.
*/
private function systemCompany(): Company
{
return Company::query()->firstOrCreate(
['slug' => 'mpba-limited'],
[
'name' => 'MPBA Limited',
'status' => 'active',
'timezone' => 'Europe/London',
'is_system_company' => true,
'billing_email' => 'mark.gregory@mpba.co.uk',
],
);
}
}
@@ -0,0 +1,123 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), RuntimeException::class, 'Spatie permission table names are not configured.');
Schema::create($tableNames['permissions'], function (Blueprint $table): void {
$table->bigIncrements('id');
$table->string('name');
$table->string('guard_name');
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames): void {
$table->bigIncrements('id');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name');
$table->string('guard_name');
$table->timestamps();
if ($teams) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams): void {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id')
->on($tableNames['permissions'])
->cascadeOnDelete();
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams): void {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id')
->on($tableNames['roles'])
->cascadeOnDelete();
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], 'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission): void {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id')
->on($tableNames['permissions'])
->cascadeOnDelete();
$table->foreign($pivotRole)
->references('id')
->on($tableNames['roles'])
->cascadeOnDelete();
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')->store(config('permission.cache.store') !== 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
Schema::dropIfExists($tableNames['role_has_permissions']);
Schema::dropIfExists($tableNames['model_has_roles']);
Schema::dropIfExists($tableNames['model_has_permissions']);
Schema::dropIfExists($tableNames['roles']);
Schema::dropIfExists($tableNames['permissions']);
}
};
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTenantsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('tenants', function (Blueprint $table) {
$table->string('id')->primary();
// your custom columns may go here
$table->timestamps();
$table->json('data')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('tenants');
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateDomainsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('domains', function (Blueprint $table) {
$table->increments('id');
$table->string('domain', 255)->unique();
$table->string('tenant_id');
$table->timestamps();
$table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('domains');
}
}
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTenantUserImpersonationTokensTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('tenant_user_impersonation_tokens', function (Blueprint $table) {
$table->string('token', 128)->primary();
$table->string('tenant_id');
$table->string('user_id');
$table->string('auth_guard');
$table->string('redirect_url');
$table->timestamp('created_at');
$table->foreign('tenant_id')->references('id')->on('tenants')->onUpdate('cascade')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('tenant_user_impersonation_tokens');
}
}
@@ -11,11 +11,27 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->text('two_factor_secret')->after('password')->nullable();
$table->text('two_factor_recovery_codes')->after('two_factor_secret')->nullable();
$table->timestamp('two_factor_confirmed_at')->after('two_factor_recovery_codes')->nullable();
});
if (! Schema::hasTable('users')) {
return;
}
if (! Schema::hasColumn('users', 'two_factor_secret')) {
Schema::table('users', function (Blueprint $table): void {
$table->text('two_factor_secret')->nullable()->after('password');
});
}
if (! Schema::hasColumn('users', 'two_factor_recovery_codes')) {
Schema::table('users', function (Blueprint $table): void {
$table->text('two_factor_recovery_codes')->nullable()->after('two_factor_secret');
});
}
if (! Schema::hasColumn('users', 'two_factor_confirmed_at')) {
Schema::table('users', function (Blueprint $table): void {
$table->timestamp('two_factor_confirmed_at')->nullable()->after('two_factor_recovery_codes');
});
}
}
/**
@@ -23,12 +39,16 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn([
'two_factor_secret',
'two_factor_recovery_codes',
'two_factor_confirmed_at',
]);
});
if (! Schema::hasTable('users')) {
return;
}
foreach (['two_factor_confirmed_at', 'two_factor_recovery_codes', 'two_factor_secret'] as $column) {
if (Schema::hasColumn('users', $column)) {
Schema::table('users', function (Blueprint $table) use ($column): void {
$table->dropColumn($column);
});
}
}
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* This migration is intentionally a no-op.
*
* The two-factor columns are managed by:
* 2025_08_14_170933_add_two_factor_columns_to_users_table.php
*
* Keeping this file prevents migration-history churn while avoiding duplicate
* column failures in SQLite, MySQL and PostgreSQL.
*/
public function up(): void
{
//
}
/**
* Reverse the migration.
*/
public function down(): void
{
//
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('imports', function (Blueprint $table): void {
$table->id();
$table->timestamp('completed_at')->nullable();
$table->string('file_name');
$table->string('file_path');
$table->string('importer');
$table->unsignedInteger('processed_rows')->default(0);
$table->unsignedInteger('total_rows');
$table->unsignedInteger('successful_rows')->default(0);
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('imports');
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('media', function (Blueprint $table) {
$table->id();
$table->morphs('model');
$table->uuid()->nullable()->unique();
$table->string('collection_name');
$table->string('name');
$table->string('file_name');
$table->string('mime_type')->nullable();
$table->string('disk');
$table->string('conversions_disk')->nullable();
$table->unsignedBigInteger('size');
$table->json('manipulations');
$table->json('custom_properties');
$table->json('generated_conversions');
$table->json('responsive_images');
$table->unsignedInteger('order_column')->nullable()->index();
$table->nullableTimestamps();
});
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('exports', function (Blueprint $table): void {
$table->id();
$table->timestamp('completed_at')->nullable();
$table->string('file_disk');
$table->string('file_name')->nullable();
$table->string('exporter');
$table->unsignedInteger('processed_rows')->default(0);
$table->unsignedInteger('total_rows');
$table->unsignedInteger('successful_rows')->default(0);
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('exports');
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('teams', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->index();
$table->string('name');
$table->boolean('personal_team');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('teams');
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('failed_import_rows', function (Blueprint $table): void {
$table->id();
$table->json('data');
$table->foreignId('import_id')->constrained()->cascadeOnDelete();
$table->text('validation_error')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('failed_import_rows');
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('team_user', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id');
$table->foreignId('user_id');
$table->string('role')->nullable();
$table->timestamps();
$table->unique(['team_id', 'user_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('team_user');
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('team_invitations', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained()->cascadeOnDelete();
$table->string('email');
$table->string('role')->nullable();
$table->timestamps();
$table->unique(['team_id', 'email']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('team_invitations');
}
};
+2 -7
View File
@@ -2,9 +2,9 @@
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Modules\Platform\database\seeders\PlatformSeeder;
class DatabaseSeeder extends Seeder
{
@@ -15,11 +15,6 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
$this->call(PlatformSeeder::class);
}
}

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