Search by

okjt / laravel-kit

Juniortambo2628

The server half of the OKJT kit: workspaces and roles, version history, and the middleware that enforces them, for Laravel 12 APIs.

v0.4.0 2026-11-02 12:14 UTC

This package is auto-updated.

Last update: 2026-11-02 12:36:38 UTC


README

The server half of the OKJT kit, for Laravel 12 APIs: workspaces (the record people work on together — a project, a wedding, a site), roles in each workspace, the middleware that enforces both, and version history with restore.

composer require okjt/laravel-kit
php artisan vendor:publish --tag=okjt-config
php artisan vendor:publish --tag=okjt-migrations   # new projects: revisions + members table

The package is published from the OKJT monorepo to Juniortambo2628/okjt-laravel-kit, and installed from Packagist.

Set it up

config/okjt.php names your workspace model and tables (see the file for every setting):

'workspace' => [
    'model' => App\Models\Project::class,   // implements Okjt\Kit\Workspace\Workspace
    'foreign_key' => 'project_id',          // on every workspace-owned table
    'members_table' => 'project_user',      // project_id, user_id, role
    'header' => 'X-Workspace',              // how the app names the workspace
    'open_routes' => ['api/login', 'api/user', 'api/projects'],
],
'roles' => ['owner' => 'owner', 'viewer' => 'viewer', 'all' => ['owner', 'editor', 'viewer'], 'viewer_writes' => ['api/logout']],
'label' => 'project',                       // used in messages: "This project could not be found."
// The workspace model
class Project extends Model implements Workspace
{
    public static function findByKey(?string $key): ?self { return static::where('slug', $key)->first(); }
    public function isActive(): bool { return $this->status === 'active'; }
    public function isPublished(): bool { return true; }
}

// The user model: $user->role (in the current workspace), roleIn(), setRoleIn(), isMemberOf(), isOwner()
class User extends Authenticatable { use HasWorkspaceRoles; }

// Anything that belongs to a workspace: scoped to the current one, created in it, with history
class Expense extends Model
{
    use BelongsToWorkspace, RecordsRevisions;
    protected string $revisionModule = 'expenses';   // the dashboard module it belongs to
}
// bootstrap/app.php: pick the workspace before route-model binding
$middleware->api(prepend: [Okjt\Kit\Http\Middleware\ResolveWorkspace::class]);

// routes/api.php
Route::middleware(['auth:sanctum', EnforceRole::class])->group(function () {
    Route::get('/revisions', [RevisionController::class, 'index']);
    Route::post('/revisions/{revision}/restore', [RevisionController::class, 'restore']);
    Route::delete('/project', [ProjectController::class, 'destroy'])->middleware(EnforceRole::class.':owner');
});
Route::middleware(['auth:sanctum', EnsureSuperadmin::class])->prefix('console')->group(/* … */);

What it does

Piece Does
ResolveWorkspace Makes the workspace the request names current. Unknown → 404; none on a route that needs one → 400; inactive or unpublished → 404 for anyone who isn't one of its people. reason in the JSON tells the app which.
CurrentWorkspace The current workspace (get(), id(), run($workspace, fn)), its upload folder, and exists/unique validation rules limited to it.
BelongsToWorkspace A global scope (named by workspace.scope) so another workspace's rows are "not found", including route-model binding; new rows are created in the current one.
EnforceRole Members only; inactive workspaces refused; :owner for owners only; viewers may only read, except roles.viewer_writes.
EnsureSuperadmin users.is_superadmin only: platform tools such as a console or backups.
HasWorkspaceRoles A person's role per workspace from the members table, cached per request.
RecordsRevisions, Revision, RevisionController Every create/update/delete/restore recorded with before/after; one person's edits to one record within 10 minutes merge; versions kept 180 days (model:prune); restore one version or everything saved together. Paused while migrations run.
AuthController, TwoFactorController, EmailVerificationController Sign in with a password (then a code, if two-step verification is on), sign up, sign out, who you are; turning two-step verification on and off; verifying an email address. What signing in hands back is signedIn() (SignsPeopleIn), one method a project overrides once.
SocialAuthController Google sign-in (laravel/socialite): the app gets a one-time code, never a token, in its URL. A first sign-in creates a verified owner and calls joined().
PasskeyController Passkeys (laravel/passkeys) on bearer tokens; routes go in routes/web.php, since the challenges ride the session.
NotificationController, NotifiesWorkspace A workspace's notifications for its people: list, mark read; notifyAdmin() to send one.
BackupService, BackupController, backups:create, backups:verify Whole-database backups as JSON in a zip (any driver, no database tools needed); restore after a safety backup; verify by restoring into a throwaway database; off-site copies to a disk or by email; snapshot() before risky bulk changes. Settings in okjt.backups; extend the service for upload folders or a step after restoring. Needs ext-zip.
ConsoleController, AuditLog The platform console for its staff: every workspace (details and counts, never content), status changes with a reason, deleting after typing the address, and the audit log of all of it. Projects add their own pages beside these.
HasTeam, HasAccounts, TeamController Accounts that own workspaces. Staff work on all of an account's workspaces (new ones too) or chosen ones, with one role; owners invite them, change their access and take them off the team. Settings in okjt.accounts.
Plans, Limits, PlanLimitReached, BillingService, PaystackService, BillingController, Payment Optional billing: a plan per account with limits (a trial while billing is off), checks before adding what a plan counts, and payments through Paystack (checkout, verify, signed webhook), each switching a plan on exactly once. Settings in okjt.billing.
ApiResponse The { success, message, data } envelope the dashboard kit reads.

When a user is deleted, set their revisions.user_id to null and delete their members rows (the migrations use plain columns, not foreign keys).

Tests

composer install && vendor/bin/phpunit