The BDM Universal API is a standardized framework that provides consistent API patterns, authentication, and permission management across all BDM modules.
Standard CRUD operations, pagination, filtering, and response formatting
Sanctum tokens, API keys, and guest access tokens
User-to-user sharing with role-based access control
Token-based external access with security layers
Request logging, rate limiting, and analytics
php artisan migrate
php artisan vendor:publish --tag=bdm-api-config
composer require laravel/sanctum php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" php artisan migrate
Edit config/cors.php:
'paths' => ['api/*'], 'allowed_methods' => ['*'], 'allowed_origins' => ['*'], // Or specify your domains 'allowed_headers' => ['*'],
# .env SANCTUM_STATEFUL_DOMAINS=localhost,127.0.0.1 SESSION_DOMAIN=localhost # API Settings BDM_API_RATE_LIMIT_PER_MINUTE=100 BDM_API_RATE_LIMIT_PER_HOUR=5000
app/
├── BDMCore/ # Universal Framework
│ ├── API/
│ │ ├── Controllers/
│ │ │ └── BaseApiController.php
│ │ ├── Middleware/
│ │ │ ├── AuthenticateApi.php
│ │ │ ├── CheckModulePermission.php
│ │ │ └── LogApiRequest.php
│ │ └── Traits/
│ │ ├── HasStandardizedApi.php
│ │ └── HasModulePermissions.php
│ │
│ ├── Services/
│ │ ├── PermissionService.php
│ │ ├── GuestAccessService.php
│ │ └── ApiLoggerService.php
│ │
│ └── Models/
│ ├── ModulePermission.php
│ ├── GuestAccessToken.php
│ └── ApiRequestLog.php
│
├── BDMAccounting/ # Example Module
│ ├── API/
│ │ └── Controllers/
│ │ └── InvoiceApiController.php
│ ├── Models/
│ │ └── Invoice.php
│ └── Routes/
│ └── api.php
│
└── BDMStockControl/ # Another Module
└── ...same structure
💡 Pattern: Each module has its own API/Controllers directory that extends BaseApiController.
⚠️ Important: Every BDM module must follow this structure to integrate with the Universal API.
app/
└── BDMYourModule/
├── API/
│ └── Controllers/
├── Models/
├── Services/
└── Routes/
└── api.php
Create config/modules/bdm_yourmodule_permissions.php:
<?php
return [
'roles' => [
'owner' => [
'label' => 'Owner',
'permissions' => ['*'],
],
'admin' => [
'label' => 'Admin',
'permissions' => [
'products.view',
'products.create',
'products.edit',
'products.delete',
'reports.view',
],
],
'manager' => [
'label' => 'Manager',
'permissions' => [
'products.view',
'products.create',
'products.edit',
'reports.view',
],
],
'staff' => [
'label' => 'Staff',
'permissions' => [
'products.view',
'products.create',
],
],
'viewer' => [
'label' => 'Viewer',
'permissions' => [
'products.view',
],
],
],
];
Create config/modules/bdm_yourmodule_api.php:
<?php
return [
'searchable_fields' => ['name', 'sku', 'description'],
'sortable_fields' => ['name', 'price', 'created_at'],
'filterable_fields' => ['category', 'status', 'brand'],
'default_per_page' => 15,
'max_per_page' => 100,
];
Add trait to your model:
<?php
namespace App\BDMYourModule\Models;
use Illuminate\Database\Eloquent\Model;
use App\BDMCore\API\Traits\HasModulePermissions;
class Product extends Model
{
use HasModulePermissions;
protected $table = 'bdm_yourmodule_products';
protected $fillable = [
'name',
'sku',
'price',
'user_id', // Important: Owner field
];
// Define module slug
public function getModuleSlug(): string
{
return 'yourmodule';
}
}
See API Controller section below for details.
Create app/BDMYourModule/Routes/api.php:
<?php
use Illuminate\Support\Facades\Route;
use App\BDMYourModule\API\Controllers\ProductApiController;
Route::prefix('v1/yourmodule')
->middleware(['auth:sanctum', 'log.api'])
->group(function () {
Route::apiResource('products', ProductApiController::class);
});
Then load in routes/api.php:
require app_path('BDMYourModule/Routes/api.php');
<?php
namespace App\BDMYourModule\API\Controllers;
use App\BDMCore\API\Controllers\BaseApiController;
use App\BDMYourModule\Models\Product;
use Illuminate\Http\Request;
class ProductApiController extends BaseApiController
{
protected $model = Product::class;
protected $moduleSlug = 'yourmodule';
/**
* Validation rules for create
*/
protected function createRules(): array
{
return [
'name' => 'required|string|max:255',
'sku' => 'required|string|unique:bdm_yourmodule_products',
'price' => 'required|numeric|min:0',
];
}
/**
* Validation rules for update
*/
protected function updateRules(): array
{
return [
'name' => 'sometimes|string|max:255',
'sku' => 'sometimes|string',
'price' => 'sometimes|numeric|min:0',
];
}
}
✓ That's it! By extending BaseApiController, you automatically get:
/**
* Custom action: Mark product as featured
*/
public function markFeatured(Request $request, $id)
{
// Check permission
if (!$this->canPerformAction('products.edit')) {
return $this->errorResponse('Unauthorized', 403);
}
$product = $this->getModelQuery()->findOrFail($id);
$product->update(['is_featured' => true]);
return $this->successResponse(
$product,
'Product marked as featured'
);
}
/**
* Customize base query
*/
protected function getModelQuery()
{
return parent::getModelQuery()
->with(['category', 'supplier']) // Eager load
->where('status', 'active'); // Filter
}
public function someAction(Request $request)
{
// Check if user can perform action
if (!$this->canPerformAction('products.delete')) {
return $this->errorResponse('Unauthorized', 403);
}
// Your logic here
}
use App\BDMCore\Services\PermissionService;
public function index(PermissionService $permissionService)
{
$user = auth()->user();
// Check if user has access to module
if (!$permissionService->hasAccess($user, 'yourmodule')) {
return $this->errorResponse('No access', 403);
}
// Get effective user ID (owner if accessing via shared permission)
$effectiveUserId = $permissionService->getEffectiveUserId(
$user,
'yourmodule'
);
// Query using effective user ID
$products = Product::where('user_id', $effectiveUserId)->get();
return $this->successResponse($products);
}
use App\BDMCore\Services\PermissionService;
use App\BDMCore\Enums\PermissionRole;
$permissionService = app(PermissionService::class);
$permission = $permissionService->shareModule(
owner: auth()->user(),
sharedWithEmail: 'colleague@example.com',
moduleSlug: 'yourmodule',
role: PermissionRole::MANAGER,
expiresAt: now()->addMonths(6)
);
// In routes/api.php
Route::middleware(['auth:sanctum', 'module.permission:yourmodule'])
->group(function () {
Route::apiResource('products', ProductApiController::class);
});
use App\BDMCore\Services\GuestAccessService;
public function createGuestAccess(Request $request, $id)
{
$product = Product::findOrFail($id);
$guestAccessService = app(GuestAccessService::class);
$token = $guestAccessService->createToken(
user: auth()->user(),
resourceType: 'yourmodule_product',
resourceId: $product->id,
guestEmail: $request->guest_email,
guestName: $request->guest_name,
permissions: ['view', 'download'],
expiresAt: now()->addDays(30),
requiresPassword: true,
password: $request->password
);
// Send email to guest
Mail::to($request->guest_email)->send(
new GuestAccessEmail($token)
);
return $this->successResponse([
'token' => $token->token,
'link' => route('guest.access', $token->token)
]);
}
Create app/BDMCore/GuestAccess/Handlers/YourModuleProductHandler.php:
<?php
namespace App\BDMCore\GuestAccess\Handlers;
use App\BDMCore\GuestAccess\Contracts\GuestAccessHandler;
use App\BDMCore\Models\GuestAccessToken;
use App\BDMYourModule\Models\Product;
class YourModuleProductHandler implements GuestAccessHandler
{
public function handle(GuestAccessToken $token): array
{
$product = Product::findOrFail($token->resource_id);
// Mask sensitive data
return [
'id' => $product->id,
'name' => $product->name,
'price' => $product->price,
// Don't include cost, supplier_id, etc.
];
}
public function generateDownload(GuestAccessToken $token): string
{
$product = Product::findOrFail($token->resource_id);
// Generate PDF
$pdf = PDF::loadView('products.pdf', compact('product'));
return $pdf->download('product-' . $product->id . '.pdf');
}
public function executeAction(
GuestAccessToken $token,
string $action,
array $data
): array {
if ($action === 'request_quote') {
// Handle quote request
// ...
return ['success' => true, 'message' => 'Quote requested'];
}
throw new \Exception('Action not allowed');
}
}
In app/Providers/AppServiceProvider.php:
use App\BDMCore\Services\GuestAccessService;
use App\BDMCore\GuestAccess\Handlers\YourModuleProductHandler;
public function boot()
{
GuestAccessService::registerHandler(
'yourmodule_product',
YourModuleProductHandler::class
);
}
use App\BDMCore\Services\WebhookService;
public function store(Request $request)
{
$product = Product::create($request->validated());
// Trigger webhook
app(WebhookService::class)->dispatch(
event: 'product.created',
payload: [
'product_id' => $product->id,
'name' => $product->name,
'price' => $product->price,
],
userId: auth()->id()
);
return $this->successResponse($product, 'Product created');
}
Define webhook events in your module config:
// config/modules/bdm_yourmodule_webhooks.php
return [
'events' => [
'product.created' => 'Product Created',
'product.updated' => 'Product Updated',
'product.deleted' => 'Product Deleted',
'order.placed' => 'Order Placed',
'order.shipped' => 'Order Shipped',
],
];
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use App\BDMYourModule\Models\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ProductApiTest extends TestCase
{
use RefreshDatabase;
public function test_can_list_products()
{
$user = User::factory()->create();
Product::factory()->count(5)->create(['user_id' => $user->id]);
$response = $this->actingAs($user, 'sanctum')
->getJson('/api/v1/yourmodule/products');
$response->assertOk()
->assertJsonStructure([
'success',
'data' => [
'*' => ['id', 'name', 'price']
]
]);
}
public function test_can_create_product()
{
$user = User::factory()->create();
$response = $this->actingAs($user, 'sanctum')
->postJson('/api/v1/yourmodule/products', [
'name' => 'Test Product',
'sku' => 'TEST-001',
'price' => 99.99
]);
$response->assertCreated()
->assertJson([
'success' => true,
'data' => ['name' => 'Test Product']
]);
$this->assertDatabaseHas('bdm_yourmodule_products', [
'name' => 'Test Product'
]);
}
public function test_cannot_access_other_users_products()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$product = Product::factory()->create(['user_id' => $user1->id]);
$response = $this->actingAs($user2, 'sanctum')
->getJson("/api/v1/yourmodule/products/{$product->id}");
$response->assertNotFound();
}
}
# Run all tests php artisan test # Run specific test file php artisan test tests/Feature/ProductApiTest.php # Run with coverage php artisan test --coverage
Cause: Missing or invalid authentication token
Solution:
Cause: User doesn't have permission
Solution:
Cause: Rate limit exceeded
Solution:
Cause: Cross-origin request blocked
Solution:
# Enable query logging DB::enableQueryLog(); // ... your code dd(DB::getQueryLog()); # Check API logs tail -f storage/logs/laravel.log # View request logs in database SELECT * FROM bdm_api_request_logs WHERE user_id = ? ORDER BY created_at DESC LIMIT 50;
php artisan migrate
php artisan test
php artisan cache:clear
config/bdm_api.php
config/modules/*_permissions.php
app/BDMCore/API/Controllers/BaseApiController.php