Skip to content

Commit 421264d

Browse files
feat(worbench-setup): make plugin development self-contained (EC-197)
* feat(worbench-setup): prepare base setup * chore(workbench-setup): setup fixes * chore(workbench-setup): fixes * chore(workbench-setup): further fixes * fix(migrations): only create table if not already * chore(workbench-setup): final fixes * fix(workbench-setup): fix composer test and format * fix(workflows): fix github workflows * chore(workbench-setup): remove config files * chore(workbench-setup): seeder fixes * build(workbench): set more gitignores * build(workbench): fix workbench .env file setup * build(workbench): add permissions migration * build(workbench): fix app migrations * fix: setup fixes * fix: more fixes :( * fix: remove duplicate lando file * fix: remove duplicate migration * fix: remove publishing from composer scirpt * fix: fix ci * fix: more test fixes * fix: revert tests changes * fix: fix phpunit file * fix: add ci setup composer * Revert "fix: add ci setup composer" This reverts commit 8d76155. * fix: conditional publishing of permissions * Revert "fix: conditional publishing of permissions" This reverts commit 099069a. * Revert "fix: remove publishing from composer scirpt" This reverts commit 527b92a. * fix: add empty migration, fix db namespace We need to add the /app prefix because that's the path where our project folder is mounted inside the container. * chore: add spatie laravel permission migration * chore: fix attempt of db setup * chore: more fixes * chore: add minimal composer json for workbench * chore: setup improvements & fixes * chore: permission fixes try * chore: re-add migration * chore: more fixes * build(lando): update landofile * build(workbench): add cache dir in storage * chore(readme): update workbench instructions * chore: temp commit * Revert "chore: temp commit" This reverts commit 2ff16ee. * fix: use local migrations * chore: fix spatie permission publishing * chore: remove Spatie PermissionServiceProvider from composer setup --------- Co-authored-by: Omer Šabić <[email protected]>
1 parent 0794260 commit 421264d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+3106
-52
lines changed

.lando.dist.yml

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
1-
#file: noinspection ComposeUnknownKeys
2-
name: eclipse-world
1+
#file: noinspection ComposeUnknownKeys,YAMLSchemaValidation
2+
name: eclipse-world-plugin
3+
recipe: laravel
4+
config:
5+
webroot: workbench/public
6+
php: '8.3'
7+
via: nginx
8+
database: mariadb:10.11
39
services:
410
appserver:
511
type: php:custom
612
xdebug: "debug,develop,coverage"
7-
via: cli
13+
environment:
14+
TZ: "Europe/Ljubljana"
15+
APP_BASE_PATH: "/app/workbench"
16+
TESTBENCH_WORKING_PATH: "/app"
817
overrides:
9-
image: slimdeluxe/php:8.2-v1.1
18+
image: slimdeluxe/php:8.3-v1.3
19+
platform: linux/amd64
20+
run:
21+
- composer install --no-interaction --prefer-dist
22+
- composer run setup --no-interaction
23+
- composer run build --no-interaction
24+
appserver_nginx:
25+
scanner:
26+
path: /admin/login
27+
retry: 5
1028
tooling:
1129
php:
1230
service: appserver

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,25 @@ Should you want to contribute, please see the development guidelines in the [Dat
5959
````
6060
4. You can now develop and run tests. Happy coding 😉
6161

62+
#### Workbench + Lando (browser testing)
63+
64+
This package ships with a minimal Testbench Workbench so you can run the Filament UI without a separate app:
65+
66+
1. Clone the repository
67+
2. Start the container
68+
```shell
69+
lando start
70+
```
71+
72+
3. Open the admin panel at `https://eclipse-world-plugin.lndo.site/admin`
73+
74+
**No login required** - you'll be automatically signed in as a test user with full permissions.
75+
76+
Notes:
77+
- The container serves `workbench/public` as the webroot.
78+
- Use `lando test` for `package:test` and `lando testbench` for other Testbench commands.
79+
- No Telescope, websockets or health checks are enabled to keep the setup minimal.
80+
6281
💡 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).
6382
However, the plugin should be universal and not dependent on our app setup or core package.
6483

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,11 @@
7272
"@php vendor/bin/testbench serve --ansi"
7373
],
7474
"setup": [
75+
"@php -r \"if (!file_exists('workbench/.env')) { copy('workbench/.env.example', 'workbench/.env'); echo '.env file created from .env.example\\n'; }\"",
76+
"@php vendor/bin/testbench key:generate --ansi",
7577
"npm install",
76-
"@php vendor/bin/testbench vendor:publish --provider='Spatie\\Permission\\PermissionServiceProvider'",
7778
"@php vendor/bin/testbench vendor:publish --tag='filament-shield-config'",
79+
"@php vendor/bin/testbench filament:assets",
7880
"@php vendor/bin/testbench package:sync-skeleton"
7981
]
8082
},

phpunit.xml.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<server name="BCRYPT_ROUNDS" value="4"/>
2020
<server name="CACHE_STORE" value="array"/>
2121
<server name="DB_CONNECTION" value="sqlite"/>
22-
<server name="DB_DATABASE" value=":memory:"/>
22+
<server name="DB_DATABASE" value="workbench/database/database.sqlite"/>
2323
<server name="MAIL_MAILER" value="array"/>
2424
<server name="QUEUE_CONNECTION" value="sync"/>
2525
<server name="SESSION_DRIVER" value="array"/>

testbench.yaml

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
laravel: '@testbench'
22

3+
env:
4+
- DB_CONNECTION=sqlite
5+
- DB_DATABASE=/app/workbench/database/database.sqlite
6+
- APP_KEY=base64:ZQvPGC7uVADkjOgtGIIuCI8u3/Pzu+VaRObIbHsgjCc=
7+
- APP_ENV=local
8+
- APP_DEBUG=true
9+
- SESSION_DRIVER=database
10+
- CACHE_STORE=database
11+
- QUEUE_CONNECTION=sync
12+
313
providers:
414
- Workbench\App\Providers\WorkbenchServiceProvider
515

616
migrations:
717
- workbench/database/migrations
8-
9-
seeders:
10-
- Workbench\Database\Seeders\DatabaseSeeder
18+
- database/migrations
1119

1220
workbench:
1321
start: '/'
@@ -16,7 +24,7 @@ workbench:
1624
discovers:
1725
web: true
1826
api: false
19-
commands: false
27+
commands: true
2028
components: false
2129
views: false
2230
build:
@@ -26,7 +34,3 @@ workbench:
2634
- migrate-fresh
2735
assets:
2836
- laravel-assets
29-
sync:
30-
- from: storage
31-
to: workbench/storage
32-
reverse: true

workbench/.env.example

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
APP_NAME=Laravel
22
APP_ENV=local
3-
APP_KEY=AckfSECXIvnK5r28GVIWUAxmbBSjTsmF
3+
APP_KEY=
44
APP_DEBUG=true
5-
APP_URL=http://localhost
5+
APP_URL=https://eclipse-world-plugin.lndo.site
66

77
APP_LOCALE=en
88
APP_FALLBACK_LOCALE=en
@@ -21,23 +21,26 @@ LOG_DEPRECATIONS_CHANNEL=null
2121
LOG_LEVEL=debug
2222

2323
DB_CONNECTION=sqlite
24+
DB_DATABASE=/app/workbench/database/database.sqlite
2425
# DB_HOST=127.0.0.1
2526
# DB_PORT=3306
26-
# DB_DATABASE=laravel
2727
# DB_USERNAME=root
2828
# DB_PASSWORD=
2929

30-
SESSION_DRIVER=cookie
30+
SESSION_DRIVER=database
3131
SESSION_LIFETIME=120
3232
SESSION_ENCRYPT=false
3333
SESSION_PATH=/
34-
SESSION_DOMAIN=null
34+
SESSION_DOMAIN=.eclipse-world-plugin.lndo.site
35+
SESSION_SECURE_COOKIE=true
36+
SESSION_HTTP_ONLY=true
37+
SESSION_SAME_SITE=lax
3538

3639
BROADCAST_CONNECTION=log
3740
FILESYSTEM_DISK=local
38-
QUEUE_CONNECTION=database
41+
QUEUE_CONNECTION=sync
3942

40-
CACHE_STORE=database
43+
CACHE_STORE=array
4144
# CACHE_PREFIX=
4245

4346
MEMCACHED_HOST=127.0.0.1

workbench/.gitignore

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
11
.env
22
.env.dusk
3-
storage
3+
storage/app/*
4+
!storage/app/.gitkeep
5+
!storage/app/public/
6+
storage/app/public/*
7+
!storage/app/public/.gitkeep
8+
storage/framework/sessions/*
9+
!storage/framework/sessions/.gitkeep
10+
storage/framework/testing/*
11+
!storage/framework/testing/.gitkeep
12+
storage/framework/views/*
13+
!storage/framework/views/.gitkeep
14+
storage/logs/*
15+
!storage/logs/.gitkeep
16+
stubs/
17+
resources/views/vendor/
18+
lang/
19+
vendor
20+
public/build
21+
public/hot
22+
/public/css
23+
/public/js
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
3+
namespace Workbench\App\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Support\Facades\Artisan;
8+
use Illuminate\Support\Facades\Auth;
9+
use Illuminate\Support\Facades\Cache;
10+
use Illuminate\Support\Facades\DB;
11+
use Illuminate\Support\Facades\Hash;
12+
use Illuminate\Support\Facades\Log;
13+
use Spatie\Permission\Models\Permission;
14+
use Spatie\Permission\Models\Role;
15+
use Spatie\Permission\PermissionRegistrar;
16+
use Workbench\App\Models\User;
17+
18+
class WorkbenchBootstrap
19+
{
20+
public function handle(Request $request, Closure $next)
21+
{
22+
if (config('app.env') === 'local' && ! Auth::guard('web')->check()) {
23+
$user = User::query()->first();
24+
25+
if (! $user) {
26+
try {
27+
$user = User::query()->firstOrCreate(
28+
['email' => '[email protected]'],
29+
[
30+
'name' => 'Admin User',
31+
'password' => Hash::make('password'),
32+
'email_verified_at' => now(),
33+
],
34+
);
35+
} catch (\Throwable $e) {
36+
Log::error('[Workbench] User creation failed', ['message' => $e->getMessage()]);
37+
// In case of a race/unique constraint, fetch the existing one
38+
$user = User::query()->where('email', '[email protected]')->first();
39+
}
40+
}
41+
42+
if ($user) {
43+
$this->bootstrapPermissionsAndAssign($user);
44+
Auth::guard('web')->login($user);
45+
$request->session()->regenerate();
46+
47+
if ($request->is('admin/login')) {
48+
return redirect()->to('/admin');
49+
}
50+
}
51+
}
52+
53+
return $next($request);
54+
}
55+
56+
private function bootstrapPermissionsAndAssign(User $user): void
57+
{
58+
// Use cache to prevent running this multiple times
59+
$cacheKey = 'workbench:permissions:bootstrapped';
60+
61+
if (Cache::has($cacheKey)) {
62+
// Just ensure user has roles if already bootstrapped
63+
$this->ensureUserHasRoles($user);
64+
65+
return;
66+
}
67+
68+
try {
69+
// Use lock to prevent concurrent execution
70+
Cache::lock('workbench:bootstrap-permissions', 30)->block(10, function () use ($user, $cacheKey) {
71+
// Double-check inside the lock
72+
if (Cache::has($cacheKey)) {
73+
return;
74+
}
75+
76+
// Normalize guards first
77+
DB::table('permissions')->whereNull('guard_name')->orWhere('guard_name', '')->update(['guard_name' => 'web']);
78+
DB::table('roles')->whereNull('guard_name')->orWhere('guard_name', '')->update(['guard_name' => 'web']);
79+
80+
// Generate Filament Shield permissions if none exist yet
81+
if (Permission::query()->count() === 0) {
82+
Artisan::call('shield:generate', [
83+
'--all' => true,
84+
'--panel' => 'admin',
85+
]);
86+
}
87+
88+
// Reset caches/registrar to ensure guards are picked up
89+
Artisan::call('permission:cache-reset');
90+
app(PermissionRegistrar::class)->forgetCachedPermissions();
91+
92+
// Ensure roles with correct guard
93+
$this->ensureRolesHaveCorrectGuard();
94+
95+
// Create roles
96+
$superAdmin = Role::firstOrCreate(['name' => 'super_admin', 'guard_name' => 'web']);
97+
$panelUser = Role::firstOrCreate(['name' => 'panel_user', 'guard_name' => 'web']);
98+
99+
// Only assign permissions if the role doesn't already have them
100+
$this->assignPermissionsToRole($superAdmin);
101+
102+
// Assign roles to user
103+
$this->ensureUserHasRoles($user);
104+
105+
// Mark as bootstrapped (cache for 1 hour)
106+
Cache::put($cacheKey, true, 3600);
107+
});
108+
} catch (\Throwable $e) {
109+
Log::error('[Workbench] Bootstrap permissions failed', ['message' => $e->getMessage()]);
110+
}
111+
}
112+
113+
private function ensureRolesHaveCorrectGuard(): void
114+
{
115+
$existingSuper = Role::where('name', 'super_admin')->first();
116+
if ($existingSuper && $existingSuper->guard_name !== 'web') {
117+
$existingSuper->guard_name = 'web';
118+
$existingSuper->save();
119+
}
120+
121+
$existingPanel = Role::where('name', 'panel_user')->first();
122+
if ($existingPanel && $existingPanel->guard_name !== 'web') {
123+
$existingPanel->guard_name = 'web';
124+
$existingPanel->save();
125+
}
126+
}
127+
128+
private function assignPermissionsToRole(Role $role): void
129+
{
130+
// Check if role already has permissions to avoid duplicate inserts
131+
if ($role->permissions()->count() > 0) {
132+
return;
133+
}
134+
135+
$permissions = Permission::query()->where('guard_name', 'web')->get();
136+
if ($permissions->isNotEmpty()) {
137+
// Use DB transaction to ensure atomicity
138+
DB::transaction(function () use ($role, $permissions) {
139+
// Clear existing permissions first to avoid duplicates
140+
$role->permissions()->detach();
141+
142+
// Batch insert to avoid individual constraint violations
143+
$pivotData = $permissions->map(function ($permission) use ($role) {
144+
return [
145+
'role_id' => $role->id,
146+
'permission_id' => $permission->id,
147+
];
148+
})->toArray();
149+
150+
// Use insert ignore equivalent for SQLite
151+
foreach ($pivotData as $data) {
152+
DB::table('role_has_permissions')
153+
->insertOrIgnore($data);
154+
}
155+
});
156+
}
157+
}
158+
159+
private function ensureUserHasRoles(User $user): void
160+
{
161+
$superAdmin = Role::where('name', 'super_admin')->where('guard_name', 'web')->first();
162+
$panelUser = Role::where('name', 'panel_user')->where('guard_name', 'web')->first();
163+
164+
$rolesToAssign = collect([$superAdmin, $panelUser])
165+
->filter()
166+
->pluck('name')
167+
->toArray();
168+
169+
if (! empty($rolesToAssign)) {
170+
// Only sync if user doesn't already have these roles
171+
$existingRoles = $user->roles()->pluck('name')->toArray();
172+
$missingRoles = array_diff($rolesToAssign, $existingRoles);
173+
174+
if (! empty($missingRoles)) {
175+
$user->assignRole($missingRoles);
176+
}
177+
}
178+
}
179+
}

0 commit comments

Comments
 (0)