Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
2e38866
feat(worbench-setup): prepare base setup
KilianTrunk Aug 9, 2025
64917d2
chore(workbench-setup): setup fixes
KilianTrunk Aug 9, 2025
d92393a
chore(workbench-setup): fixes
KilianTrunk Aug 10, 2025
919cb87
chore(workbench-setup): further fixes
KilianTrunk Aug 10, 2025
ac3d8c9
fix(migrations): only create table if not already
KilianTrunk Aug 10, 2025
f73d10a
chore(workbench-setup): final fixes
KilianTrunk Aug 11, 2025
4fe23d8
fix(workbench-setup): fix composer test and format
KilianTrunk Aug 11, 2025
2488f55
fix(workflows): fix github workflows
KilianTrunk Aug 11, 2025
9de4466
chore(workbench-setup): remove config files
KilianTrunk Aug 16, 2025
ed3b2b0
chore(workbench-setup): seeder fixes
KilianTrunk Aug 16, 2025
3c46c3c
build(workbench): set more gitignores
SlimDeluxe Aug 20, 2025
d865d36
build(workbench): fix workbench .env file setup
SlimDeluxe Aug 20, 2025
496d65c
build(workbench): add permissions migration
SlimDeluxe Aug 20, 2025
c0449c1
build(workbench): fix app migrations
SlimDeluxe Aug 20, 2025
509d473
fix: setup fixes
KilianTrunk Aug 21, 2025
dbd8516
fix: more fixes :(
KilianTrunk Aug 21, 2025
4b4c382
fix: remove duplicate lando file
KilianTrunk Aug 21, 2025
787aece
fix: remove duplicate migration
KilianTrunk Aug 21, 2025
527b92a
fix: remove publishing from composer scirpt
KilianTrunk Aug 21, 2025
07d0296
fix: fix ci
KilianTrunk Aug 21, 2025
d6dd7b6
fix: more test fixes
KilianTrunk Aug 21, 2025
7493467
fix: revert tests changes
KilianTrunk Aug 21, 2025
6cf7a1c
fix: fix phpunit file
KilianTrunk Aug 21, 2025
8d76155
fix: add ci setup composer
KilianTrunk Aug 21, 2025
0503693
Revert "fix: add ci setup composer"
KilianTrunk Aug 21, 2025
099069a
fix: conditional publishing of permissions
KilianTrunk Aug 21, 2025
f571284
Revert "fix: conditional publishing of permissions"
KilianTrunk Aug 21, 2025
2b8ad07
Revert "fix: remove publishing from composer scirpt"
KilianTrunk Aug 21, 2025
bd34417
fix: add empty migration, fix db namespace
KilianTrunk Aug 21, 2025
18d3422
chore: add spatie laravel permission migration
KilianTrunk Aug 28, 2025
9723879
chore: fix attempt of db setup
KilianTrunk Aug 28, 2025
d54f0af
chore: more fixes
KilianTrunk Aug 28, 2025
a717169
chore: add minimal composer json for workbench
KilianTrunk Aug 28, 2025
6a29503
Merge branch 'main' into feat/workbench-setup
KilianTrunk Aug 28, 2025
79e0c66
chore: setup improvements & fixes
KilianTrunk Aug 28, 2025
af00a7f
chore: permission fixes try
KilianTrunk Aug 28, 2025
c9c9b1e
chore: re-add migration
KilianTrunk Aug 28, 2025
af1291c
chore: more fixes
KilianTrunk Aug 28, 2025
16e07f6
build(lando): update landofile
SlimDeluxe Aug 28, 2025
fbf57b8
build(workbench): add cache dir in storage
SlimDeluxe Aug 28, 2025
50df5bf
chore(readme): update workbench instructions
KilianTrunk Aug 28, 2025
2ff16ee
chore: temp commit
KilianTrunk Aug 28, 2025
9763f18
Revert "chore: temp commit"
KilianTrunk Aug 28, 2025
eb5e9cd
fix: use local migrations
KilianTrunk Aug 28, 2025
20193a3
chore: fix spatie permission publishing
KilianTrunk Aug 28, 2025
1e2758a
chore: remove Spatie PermissionServiceProvider from composer setup
KilianTrunk Aug 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions .lando.dist.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
#file: noinspection ComposeUnknownKeys
name: eclipse-world
#file: noinspection ComposeUnknownKeys,YAMLSchemaValidation
name: eclipse-world-plugin
recipe: laravel
config:
webroot: workbench/public
php: '8.3'
via: nginx
database: mariadb:10.11
services:
appserver:
type: php:custom
xdebug: "debug,develop,coverage"
via: cli
environment:
TZ: "Europe/Ljubljana"
APP_BASE_PATH: "/app/workbench"
TESTBENCH_WORKING_PATH: "/app"
overrides:
image: slimdeluxe/php:8.2-v1.1
image: slimdeluxe/php:8.3-v1.3
platform: linux/amd64
run:
- composer install --no-interaction --prefer-dist
- composer run setup --no-interaction
- composer run build --no-interaction
appserver_nginx:
scanner:
path: /admin/login
retry: 5
tooling:
php:
service: appserver
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ Should you want to contribute, please see the development guidelines in the [Dat
````
4. You can now develop and run tests. Happy coding 😉

#### Workbench + Lando (browser testing)

This package ships with a minimal Testbench Workbench so you can run the Filament UI without a separate app:

1. Clone the repository
2. Start the container
```shell
lando start
```

3. Open the admin panel at `https://eclipse-world-plugin.lndo.site/admin`

**No login required** - you'll be automatically signed in as a test user with full permissions.

Notes:
- The container serves `workbench/public` as the webroot.
- Use `lando test` for `package:test` and `lando testbench` for other Testbench commands.
- No Telescope, websockets or health checks are enabled to keep the setup minimal.

💡 To manually test the plugin in the browser, see our [recommendation](https://github.com/DataLinx/eclipsephp-core/blob/main/docs/Documentation.md#-plugin-development), which is also [how Filament suggests package development](https://filamentphp.com/docs/3.x/support/contributing#developing-with-a-local-copy-of-filament).
However, the plugin should be universal and not dependent on our app setup or core package.

Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,11 @@
"@php vendor/bin/testbench serve --ansi"
],
"setup": [
"@php -r \"if (!file_exists('workbench/.env')) { copy('workbench/.env.example', 'workbench/.env'); echo '.env file created from .env.example\\n'; }\"",
"@php vendor/bin/testbench key:generate --ansi",
"npm install",
"@php vendor/bin/testbench vendor:publish --provider='Spatie\\Permission\\PermissionServiceProvider'",
"@php vendor/bin/testbench vendor:publish --tag='filament-shield-config'",
"@php vendor/bin/testbench filament:assets",
"@php vendor/bin/testbench package:sync-skeleton"
]
},
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_STORE" value="array"/>
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
<server name="DB_DATABASE" value="workbench/database/database.sqlite"/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about why you switched to a file instead of :memory:, there must have been a reason? To my surprise both are equally fast (/slow).

<server name="MAIL_MAILER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
Expand Down
20 changes: 12 additions & 8 deletions testbench.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
laravel: '@testbench'

env:
- DB_CONNECTION=sqlite
- DB_DATABASE=/app/workbench/database/database.sqlite
- APP_KEY=base64:ZQvPGC7uVADkjOgtGIIuCI8u3/Pzu+VaRObIbHsgjCc=
- APP_ENV=local
- APP_DEBUG=true
- SESSION_DRIVER=database
- CACHE_STORE=database
- QUEUE_CONNECTION=sync

providers:
- Workbench\App\Providers\WorkbenchServiceProvider

migrations:
- workbench/database/migrations

seeders:
- Workbench\Database\Seeders\DatabaseSeeder
- database/migrations

workbench:
start: '/'
Expand All @@ -16,7 +24,7 @@ workbench:
discovers:
web: true
api: false
commands: false
commands: true
components: false
views: false
build:
Expand All @@ -26,7 +34,3 @@ workbench:
- migrate-fresh
assets:
- laravel-assets
sync:
- from: storage
to: workbench/storage
reverse: true
17 changes: 10 additions & 7 deletions workbench/.env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=AckfSECXIvnK5r28GVIWUAxmbBSjTsmF
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_URL=https://eclipse-world-plugin.lndo.site

APP_LOCALE=en
APP_FALLBACK_LOCALE=en
Expand All @@ -21,23 +21,26 @@ LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=sqlite
DB_DATABASE=/app/workbench/database/database.sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=

SESSION_DRIVER=cookie
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
SESSION_DOMAIN=.eclipse-world-plugin.lndo.site
SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=lax

BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
QUEUE_CONNECTION=sync

CACHE_STORE=database
CACHE_STORE=array
# CACHE_PREFIX=

MEMCACHED_HOST=127.0.0.1
Expand Down
22 changes: 21 additions & 1 deletion workbench/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
.env
.env.dusk
storage
storage/app/*
!storage/app/.gitkeep
!storage/app/public/
storage/app/public/*
!storage/app/public/.gitkeep
storage/framework/sessions/*
!storage/framework/sessions/.gitkeep
storage/framework/testing/*
!storage/framework/testing/.gitkeep
storage/framework/views/*
!storage/framework/views/.gitkeep
storage/logs/*
!storage/logs/.gitkeep
stubs/
resources/views/vendor/
lang/
vendor
public/build
public/hot
/public/css
/public/js
179 changes: 179 additions & 0 deletions workbench/app/Http/Middleware/WorkbenchBootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?php

namespace Workbench\App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;
use Workbench\App\Models\User;

class WorkbenchBootstrap
{
public function handle(Request $request, Closure $next)
{
if (config('app.env') === 'local' && ! Auth::guard('web')->check()) {
$user = User::query()->first();

if (! $user) {
try {
$user = User::query()->firstOrCreate(
['email' => 'test@example.com'],
[
'name' => 'Admin User',
'password' => Hash::make('password'),
'email_verified_at' => now(),
],
);
} catch (\Throwable $e) {
Log::error('[Workbench] User creation failed', ['message' => $e->getMessage()]);
// In case of a race/unique constraint, fetch the existing one
$user = User::query()->where('email', 'test@example.com')->first();
}
}

if ($user) {
$this->bootstrapPermissionsAndAssign($user);
Auth::guard('web')->login($user);
$request->session()->regenerate();

if ($request->is('admin/login')) {
return redirect()->to('/admin');
}
}
}

return $next($request);
}

private function bootstrapPermissionsAndAssign(User $user): void
{
// Use cache to prevent running this multiple times
$cacheKey = 'workbench:permissions:bootstrapped';

if (Cache::has($cacheKey)) {
// Just ensure user has roles if already bootstrapped
$this->ensureUserHasRoles($user);

return;
}

try {
// Use lock to prevent concurrent execution
Cache::lock('workbench:bootstrap-permissions', 30)->block(10, function () use ($user, $cacheKey) {
// Double-check inside the lock
if (Cache::has($cacheKey)) {
return;
}

// Normalize guards first
DB::table('permissions')->whereNull('guard_name')->orWhere('guard_name', '')->update(['guard_name' => 'web']);
DB::table('roles')->whereNull('guard_name')->orWhere('guard_name', '')->update(['guard_name' => 'web']);

// Generate Filament Shield permissions if none exist yet
if (Permission::query()->count() === 0) {
Artisan::call('shield:generate', [
'--all' => true,
'--panel' => 'admin',
]);
}

// Reset caches/registrar to ensure guards are picked up
Artisan::call('permission:cache-reset');
app(PermissionRegistrar::class)->forgetCachedPermissions();

// Ensure roles with correct guard
$this->ensureRolesHaveCorrectGuard();

// Create roles
$superAdmin = Role::firstOrCreate(['name' => 'super_admin', 'guard_name' => 'web']);
$panelUser = Role::firstOrCreate(['name' => 'panel_user', 'guard_name' => 'web']);

// Only assign permissions if the role doesn't already have them
$this->assignPermissionsToRole($superAdmin);

// Assign roles to user
$this->ensureUserHasRoles($user);

// Mark as bootstrapped (cache for 1 hour)
Cache::put($cacheKey, true, 3600);
});
} catch (\Throwable $e) {
Log::error('[Workbench] Bootstrap permissions failed', ['message' => $e->getMessage()]);
}
}

private function ensureRolesHaveCorrectGuard(): void
{
$existingSuper = Role::where('name', 'super_admin')->first();
if ($existingSuper && $existingSuper->guard_name !== 'web') {
$existingSuper->guard_name = 'web';
$existingSuper->save();
}

$existingPanel = Role::where('name', 'panel_user')->first();
if ($existingPanel && $existingPanel->guard_name !== 'web') {
$existingPanel->guard_name = 'web';
$existingPanel->save();
}
}

private function assignPermissionsToRole(Role $role): void
{
// Check if role already has permissions to avoid duplicate inserts
if ($role->permissions()->count() > 0) {
return;
}

$permissions = Permission::query()->where('guard_name', 'web')->get();
if ($permissions->isNotEmpty()) {
// Use DB transaction to ensure atomicity
DB::transaction(function () use ($role, $permissions) {
// Clear existing permissions first to avoid duplicates
$role->permissions()->detach();

// Batch insert to avoid individual constraint violations
$pivotData = $permissions->map(function ($permission) use ($role) {
return [
'role_id' => $role->id,
'permission_id' => $permission->id,
];
})->toArray();

// Use insert ignore equivalent for SQLite
foreach ($pivotData as $data) {
DB::table('role_has_permissions')
->insertOrIgnore($data);
}
});
}
}

private function ensureUserHasRoles(User $user): void
{
$superAdmin = Role::where('name', 'super_admin')->where('guard_name', 'web')->first();
$panelUser = Role::where('name', 'panel_user')->where('guard_name', 'web')->first();

$rolesToAssign = collect([$superAdmin, $panelUser])
->filter()
->pluck('name')
->toArray();

if (! empty($rolesToAssign)) {
// Only sync if user doesn't already have these roles
$existingRoles = $user->roles()->pluck('name')->toArray();
$missingRoles = array_diff($rolesToAssign, $existingRoles);

if (! empty($missingRoles)) {
$user->assignRole($missingRoles);
}
}
}
}
Loading