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');
|
$signature = (string) $request->header('X-WMS-Signature');
|
||||||
|
|
||||||
abort_if($secret === '' || $timestamp === '' || $signature === '', 401);
|
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.');
|
abort_if(abs(now()->timestamp - strtotime($timestamp)) > 300, 401, 'Expired internal API signature.');
|
||||||
|
|
||||||
$expected = hash_hmac('sha256', $timestamp . '.' . $request->getContent(), $secret);
|
$body = $request->getContent();
|
||||||
abort_unless(hash_equals($expected, $signature), 401, 'Invalid internal API signature.');
|
$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\Concerns\PasswordValidationRules;
|
||||||
use App\Livewire\Actions\Logout;
|
use App\Livewire\Actions\Logout;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class DeleteUserForm extends Component
|
class DeleteUserForm extends Component
|
||||||
@@ -22,7 +23,13 @@ class DeleteUserForm extends Component
|
|||||||
'password' => $this->currentPasswordRules(),
|
'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);
|
$this->redirect('/', navigate: true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"fortify-development",
|
"fortify-development",
|
||||||
"laravel-best-practices",
|
"laravel-best-practices",
|
||||||
"configuring-horizon",
|
"configuring-horizon",
|
||||||
|
"octane-development",
|
||||||
"fluxui-development",
|
"fluxui-development",
|
||||||
"livewire-development",
|
"livewire-development",
|
||||||
"tailwindcss-development"
|
"tailwindcss-development"
|
||||||
|
|||||||
@@ -12,7 +12,14 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->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 {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
$exceptions->shouldRenderJsonWhen(
|
$exceptions->shouldRenderJsonWhen(
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use App\Models\User;
|
|||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Modules\Platform\app\Models\Company;
|
||||||
|
use Modules\Platform\app\Models\LicenseKey;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends Factory<User>
|
* @extends Factory<User>
|
||||||
@@ -25,6 +27,7 @@ class UserFactory extends Factory
|
|||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
'company_id' => fn (): int => $this->licensedCompany()->id,
|
||||||
'name' => fake()->name(),
|
'name' => fake()->name(),
|
||||||
'email' => fake()->unique()->safeEmail(),
|
'email' => fake()->unique()->safeEmail(),
|
||||||
'email_verified_at' => now(),
|
'email_verified_at' => now(),
|
||||||
@@ -33,6 +36,9 @@ class UserFactory extends Factory
|
|||||||
'two_factor_secret' => null,
|
'two_factor_secret' => null,
|
||||||
'two_factor_recovery_codes' => null,
|
'two_factor_recovery_codes' => null,
|
||||||
'two_factor_confirmed_at' => 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(),
|
'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="icon" href="/favicon.svg" type="image/svg+xml">
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
<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
|
@fonts
|
||||||
|
|
||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class AuthenticationTest extends TestCase
|
|||||||
{
|
{
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$response = $this->post(route('login.store'), [
|
$response = $this->csrfPost(route('login.store'), [
|
||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
'password' => 'password',
|
'password' => 'password',
|
||||||
]);
|
]);
|
||||||
@@ -38,7 +38,7 @@ class AuthenticationTest extends TestCase
|
|||||||
{
|
{
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$response = $this->post(route('login.store'), [
|
$response = $this->csrfPost(route('login.store'), [
|
||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
'password' => 'wrong-password',
|
'password' => 'wrong-password',
|
||||||
]);
|
]);
|
||||||
@@ -59,7 +59,7 @@ class AuthenticationTest extends TestCase
|
|||||||
|
|
||||||
$user = User::factory()->withTwoFactor()->create();
|
$user = User::factory()->withTwoFactor()->create();
|
||||||
|
|
||||||
$response = $this->post(route('login.store'), [
|
$response = $this->csrfPost(route('login.store'), [
|
||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
'password' => 'password',
|
'password' => 'password',
|
||||||
]);
|
]);
|
||||||
@@ -72,7 +72,7 @@ class AuthenticationTest extends TestCase
|
|||||||
{
|
{
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
$response = $this->actingAs($user)->post(route('logout'));
|
$response = $this->actingAs($user)->csrfPost(route('logout'));
|
||||||
|
|
||||||
$response->assertRedirect(route('home'));
|
$response->assertRedirect(route('home'));
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class PasswordResetTest extends TestCase
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$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);
|
Notification::assertSentTo($user, ResetPassword::class);
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ class PasswordResetTest extends TestCase
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$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) {
|
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
|
||||||
$response = $this->get(route('password.reset', $notification->token));
|
$response = $this->get(route('password.reset', $notification->token));
|
||||||
@@ -61,10 +61,10 @@ class PasswordResetTest extends TestCase
|
|||||||
|
|
||||||
$user = User::factory()->create();
|
$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) {
|
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
|
||||||
$response = $this->post(route('password.update'), [
|
$response = $this->csrfPost(route('password.update'), [
|
||||||
'token' => $notification->token,
|
'token' => $notification->token,
|
||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
'password' => 'password',
|
'password' => 'password',
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class RegistrationTest extends TestCase
|
|||||||
|
|
||||||
public function test_new_users_can_register(): void
|
public function test_new_users_can_register(): void
|
||||||
{
|
{
|
||||||
$response = $this->post(route('register.store'), [
|
$response = $this->csrfPost(route('register.store'), [
|
||||||
'name' => 'John Doe',
|
'name' => 'John Doe',
|
||||||
'email' => 'test@example.com',
|
'email' => 'test@example.com',
|
||||||
'password' => 'password',
|
'password' => 'password',
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class TwoFactorChallengeTest extends TestCase
|
|||||||
|
|
||||||
$user = User::factory()->withTwoFactor()->create();
|
$user = User::factory()->withTwoFactor()->create();
|
||||||
|
|
||||||
$this->post(route('login.store'), [
|
$this->csrfPost(route('login.store'), [
|
||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
'password' => 'password',
|
'password' => 'password',
|
||||||
])->assertRedirect(route('two-factor.login'));
|
])->assertRedirect(route('two-factor.login'));
|
||||||
|
|||||||
@@ -2,11 +2,39 @@
|
|||||||
|
|
||||||
namespace Tests;
|
namespace Tests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||||
use Laravel\Fortify\Features;
|
use Laravel\Fortify\Features;
|
||||||
|
|
||||||
abstract class TestCase extends BaseTestCase
|
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
|
protected function skipUnlessFortifyHas(string $feature, ?string $message = null): void
|
||||||
{
|
{
|
||||||
if (! Features::enabled($feature)) {
|
if (! Features::enabled($feature)) {
|
||||||
|
|||||||