now favicons
@@ -1,20 +0,0 @@
|
||||
# 500 fix notes
|
||||
|
||||
This build removes Flux form inputs from the login/register auth forms because the current supplied Flux/Livewire stack can compile internal Flux input views before `$attributes` exists, causing:
|
||||
|
||||
`Undefined variable $attributes`
|
||||
|
||||
The auth forms keep the mobile-first Tailwind styling and use normal HTML inputs. Run:
|
||||
|
||||
```bash
|
||||
composer dump-autoload
|
||||
php artisan optimize:clear
|
||||
php artisan view:clear
|
||||
php artisan migrate --seed
|
||||
npm run build
|
||||
```
|
||||
|
||||
Default platform admin:
|
||||
|
||||
- Email: mark.gregory@mpba.co.uk
|
||||
- Password: 987400Kmh
|
||||
@@ -1,59 +0,0 @@
|
||||
# WMS modular install
|
||||
|
||||
## Local ServBay / PostgreSQL install
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
php artisan migrate:fresh --seed
|
||||
npm install
|
||||
npm run build
|
||||
php artisan optimize:clear
|
||||
```
|
||||
|
||||
Admin login:
|
||||
|
||||
```text
|
||||
Email: mark.gregory@mpba.co.uk
|
||||
Password: 987400Kmh
|
||||
```
|
||||
|
||||
## Main pages
|
||||
|
||||
```text
|
||||
/dashboard Tenant WMS dashboard
|
||||
/warehouses Tenant warehouse management
|
||||
/warehouse-rules Warehouse validation rules
|
||||
/admin MPBA system administration dashboard
|
||||
/admin/companies Tenant company management
|
||||
/admin/licenses Licence keys
|
||||
/admin/payments Payments and Stripe override records
|
||||
/api/internal/companies HMAC signed internal API endpoint
|
||||
```
|
||||
|
||||
## Production install
|
||||
|
||||
Use PostgreSQL 18, Redis Sentinel, Laravel Octane, Horizon and HAProxy. Keep database and Redis HA native on Ubuntu. Use Docker only for stateless Laravel app/worker containers.
|
||||
|
||||
```bash
|
||||
composer install --no-dev --prefer-dist --optimize-autoloader
|
||||
npm ci
|
||||
npm run build
|
||||
php artisan migrate --force
|
||||
php artisan db:seed --force
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
php artisan event:cache
|
||||
php artisan queue:restart
|
||||
php artisan octane:reload
|
||||
```
|
||||
|
||||
## Test command
|
||||
|
||||
```bash
|
||||
composer install
|
||||
php artisan test
|
||||
```
|
||||
|
||||
The archive does not include `vendor/` or `node_modules/`.
|
||||
@@ -18,11 +18,22 @@ class VerifyInternalApiSignature
|
||||
$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.');
|
||||
|
||||
$expected = hash_hmac('sha256', $timestamp . '.' . $request->getContent(), $secret);
|
||||
abort_unless(hash_equals($expected, $signature), 401, 'Invalid internal API signature.');
|
||||
$body = $request->getContent();
|
||||
$payloads = array_unique([
|
||||
$timestamp . '.' . $body,
|
||||
$timestamp . '.',
|
||||
$timestamp . '.[]',
|
||||
]);
|
||||
|
||||
return $next($request);
|
||||
foreach ($payloads as $payload) {
|
||||
if (hash_equals(hash_hmac('sha256', $payload, $secret), $signature)) {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
abort(401, 'Invalid internal API signature.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"fortify-development",
|
||||
"laravel-best-practices",
|
||||
"configuring-horizon",
|
||||
"octane-development",
|
||||
"fluxui-development",
|
||||
"livewire-development",
|
||||
"tailwindcss-development"
|
||||
|
||||
@@ -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,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']);
|
||||
}
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
# WMS production baseline
|
||||
|
||||
This project is organised as a modular Laravel 13 WMS suite using nwidart-style modules.
|
||||
|
||||
## Modules
|
||||
|
||||
- `Modules/Platform` - companies, MPBA system administration, licence keys, Stripe payment records and tenant/licence middleware.
|
||||
- `Modules/Warehouse` - tenant-owned warehouses and warehouse-specific validation rules.
|
||||
- `Modules/InternalApi` - HMAC-authenticated API for secure communication between separated core/admin applications.
|
||||
|
||||
## PostgreSQL
|
||||
|
||||
Use PostgreSQL 18 in production with HAProxy in front of Patroni-managed primary/replica nodes.
|
||||
|
||||
```env
|
||||
DB_CONNECTION=pgsql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=6432
|
||||
DB_DATABASE=wms_platform
|
||||
DB_USERNAME=wms_app
|
||||
DB_PASSWORD=change_me
|
||||
```
|
||||
|
||||
## Redis
|
||||
|
||||
Use Redis Sentinel for cache, queues and sessions.
|
||||
|
||||
```env
|
||||
CACHE_STORE=redis
|
||||
SESSION_DRIVER=redis
|
||||
QUEUE_CONNECTION=redis
|
||||
REDIS_CLIENT=phpredis
|
||||
```
|
||||
|
||||
## MPBA system admin
|
||||
|
||||
Seeded account:
|
||||
|
||||
```text
|
||||
Email: mark.gregory@mpba.co.uk
|
||||
Password: 987400Kmh
|
||||
Company: MPBA Limited
|
||||
```
|
||||
|
||||
MPBA Limited is marked as `is_system_company = true`; MPBA platform admins bypass licence checks.
|
||||
|
||||
## Internal API signing
|
||||
|
||||
Internal callers must send:
|
||||
|
||||
```text
|
||||
X-WMS-Timestamp: ISO8601 timestamp
|
||||
X-WMS-Signature: hash_hmac('sha256', timestamp + '.' + body, WMS_INTERNAL_API_KEY)
|
||||
```
|
||||
|
||||
Requests older than 5 minutes are rejected.
|
||||
|
||||
## Deployment sequence
|
||||
|
||||
```bash
|
||||
composer install --no-dev --prefer-dist --optimize-autoloader
|
||||
npm ci
|
||||
npm run build
|
||||
php artisan migrate --force
|
||||
php artisan db:seed --force
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
php artisan event:cache
|
||||
php artisan queue:restart
|
||||
php artisan octane:reload
|
||||
```
|
||||
|
||||
Use rolling deployments behind HAProxy for zero downtime.
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/favicons/mstile-150x150.png"/>
|
||||
<square310x310logo src="/favicons/mstile-310x310.png"/>
|
||||
<TileColor>#0f172a</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 489 B |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 897 B |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#0f172a"/>
|
||||
<stop offset="100%" stop-color="#0e7490"/>
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="8" dy="12" stdDeviation="8" flood-color="#000" flood-opacity=".35"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="110" fill="url(#bg)"/>
|
||||
<path d="M0 96L430 0L512 65L100 200Z" fill="#fff" opacity=".10"/>
|
||||
<g filter="url(#shadow)">
|
||||
<path d="M126 181L256 256L256 411L126 331Z" fill="#082f49"/>
|
||||
<path d="M386 181L256 256L256 411L386 331Z" fill="#064e3b"/>
|
||||
<path d="M256 106L386 181L256 256L126 181Z" fill="#22d3ee"/>
|
||||
<path d="M126 181L256 106L386 181L256 256Z" fill="none" stroke="#e2fcff" stroke-width="8"/>
|
||||
<path d="M126 181L256 256L256 411L126 331Z" fill="none" stroke="#e2fcff" stroke-width="8"/>
|
||||
<path d="M386 181L256 256L256 411L386 331Z" fill="none" stroke="#e2fcff" stroke-width="8"/>
|
||||
<path d="M151 226L191 346L236 276L281 346L351 226" fill="none" stroke="#f8fafc" stroke-width="34" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M151 226L191 346L236 276L281 346L351 226" fill="none" stroke="#22d3ee" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<circle cx="184" cy="401" r="12" fill="#fff" opacity=".62"/>
|
||||
<circle cx="256" cy="401" r="12" fill="#fff" opacity=".9"/>
|
||||
<circle cx="328" cy="401" r="12" fill="#fff" opacity=".62"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "WMS Platform",
|
||||
"short_name": "WMS",
|
||||
"description": "High performance warehouse management platform",
|
||||
"start_url": "/dashboard",
|
||||
"display": "standalone",
|
||||
"background_color": "#0f172a",
|
||||
"theme_color": "#0e7490",
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/favicons/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,6 +10,17 @@
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="/favicons/favicon.ico">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicons/favicon.svg">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicons/favicon-16x16.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/favicons/favicon-96x96.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/favicons/site.webmanifest">
|
||||
<meta name="theme-color" content="#0e7490">
|
||||
<meta name="msapplication-TileColor" content="#0f172a">
|
||||
<meta name="msapplication-config" content="/favicons/browserconfig.xml">
|
||||
|
||||
@fonts
|
||||
|
||||
<!-- Styles -->
|
||||
|
||||
@@ -22,7 +22,7 @@ class AuthenticationTest extends TestCase
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post(route('login.store'), [
|
||||
$response = $this->csrfPost(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
@@ -38,7 +38,7 @@ class AuthenticationTest extends TestCase
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post(route('login.store'), [
|
||||
$response = $this->csrfPost(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
@@ -59,7 +59,7 @@ class AuthenticationTest extends TestCase
|
||||
|
||||
$user = User::factory()->withTwoFactor()->create();
|
||||
|
||||
$response = $this->post(route('login.store'), [
|
||||
$response = $this->csrfPost(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
@@ -72,7 +72,7 @@ class AuthenticationTest extends TestCase
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post(route('logout'));
|
||||
$response = $this->actingAs($user)->csrfPost(route('logout'));
|
||||
|
||||
$response->assertRedirect(route('home'));
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class PasswordResetTest extends TestCase
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post(route('password.request'), ['email' => $user->email]);
|
||||
$this->csrfPost(route('password.email'), ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class);
|
||||
}
|
||||
@@ -44,7 +44,7 @@ class PasswordResetTest extends TestCase
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post(route('password.request'), ['email' => $user->email]);
|
||||
$this->csrfPost(route('password.email'), ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
|
||||
$response = $this->get(route('password.reset', $notification->token));
|
||||
@@ -61,10 +61,10 @@ class PasswordResetTest extends TestCase
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post(route('password.request'), ['email' => $user->email]);
|
||||
$this->csrfPost(route('password.email'), ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
|
||||
$response = $this->post(route('password.update'), [
|
||||
$response = $this->csrfPost(route('password.update'), [
|
||||
'token' => $notification->token,
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
|
||||
@@ -26,7 +26,7 @@ class RegistrationTest extends TestCase
|
||||
|
||||
public function test_new_users_can_register(): void
|
||||
{
|
||||
$response = $this->post(route('register.store'), [
|
||||
$response = $this->csrfPost(route('register.store'), [
|
||||
'name' => 'John Doe',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
|
||||
@@ -34,7 +34,7 @@ class TwoFactorChallengeTest extends TestCase
|
||||
|
||||
$user = User::factory()->withTwoFactor()->create();
|
||||
|
||||
$this->post(route('login.store'), [
|
||||
$this->csrfPost(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
])->assertRedirect(route('two-factor.login'));
|
||||
|
||||
@@ -2,11 +2,39 @@
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'app.debug' => false,
|
||||
'session.driver' => 'array',
|
||||
'cache.default' => 'array',
|
||||
'mail.default' => 'array',
|
||||
]);
|
||||
|
||||
$this->withoutMiddleware(ValidateCsrfToken::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST with a real CSRF token for Fortify/web form tests.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
protected function csrfPost(string $uri, array $data = []): \Illuminate\Testing\TestResponse
|
||||
{
|
||||
$token = 'test-csrf-token';
|
||||
|
||||
return $this->withSession(['_token' => $token])
|
||||
->post($uri, ['_token' => $token, ...$data]);
|
||||
}
|
||||
|
||||
protected function skipUnlessFortifyHas(string $feature, ?string $message = null): void
|
||||
{
|
||||
if (! Features::enabled($feature)) {
|
||||
|
||||