I'm building a REST API with Laravel (Lumen). The idea is that this API provides the back-end for multiple food-ordering websites. They share the same back-end logic (models, controllers etc.). This way each website only needs it's own front-end application, I'm planning to use Angular for this. Each website will have it's own data (products, pages etc.), which has to be stored in different databases.
I've defined multiple connections within config/databases.php
for testing purposes. Now I can dynamically set the connection before querying the corresponding database, like this:
class ProductController extends Controller
{
/**
* Display a listing of the resource.
*
* @return Response
*/
public function index()
{
$products = new Product;
$products->setConnection('customer_two'); // <--
$products = $products->get();
return response()->json($products);
}
}
The same can be done with caching, for instance.
What's the best way to let the API know which customer's website made the request? I need to point to the right database. Also, could this approach cause any problems performance-wise?
I'd be using a 2 pronged approach to solve this problem and I would use the first without the 2nd.
The first would be based on the route that you use the request the api. For instance you could define your routes with the prefix like /api/{site}
. This way all your api endpoints would be based on the site variable requested. E.g. /api/site1/login
would use the database site1
and /api/site2/login
would use the database site2
.
The second part to this is using JWT to authenticate like you mentioned above and on each request use a Middleware to check if the authenticated user is actually part of the users for that particular site. This is only really good for the authenticated routes however and still leaves your unauthenticated routes open to abuse, however if a legitimate user is on your site and your site is requesting the data from the api you should have the correct site
data being returned and any malicious access will just be getting public data anyway.
There is potentially a third option too. with JWT you can create custom claims. These custom claims could be used to store the site being used and which database to access. I have not done this myself but have been thinking about doing something similar to authenticate the client against my apis as well as user based authentication on top of that. This would mean every endpoint would at least be client authenticated and others would be also user authenticated as well as client authenticated.
Using middleware to easily change the database connection at runtime.
Middleware: app/Http/Middleware/DatabaseConnectionChooser.php
<?php namespace App\Http\Middleware;
use Closure;
use Illuminate\Routing\Route;
class DatabaseConnectionChooser
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
/** @var Route $route */
$route = app('router')->getRoutes()->match($request);
$connection = $route->getParameter('connection');
app('db')->setDefaultConnection($connection);
return $next($request);
}
}
Add this middleware to the app/Http/Kernel.php class to the $middleware property.
protected $middleware = [
...
\App\Http\Middleware\DatabaseConnectionChooser::class,
];
Create your routes to specify the site, aka the database connection.
app/Http/routes.php
app('router')->get('/{connection}/', function () {
return app('db')->getDefaultConnection();
});
Setup your database connection in your config.
config/database.php
'connections' => [
...
'site1' => [
'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'forge1'),
'username' => env('DB_USERNAME', 'forge1'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
'strict' => false,
],
'site2' => [
'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'),
'database' => env('DB_DATABASE', 'forge2'),
'username' => env('DB_USERNAME', 'forge2'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
'strict' => false,
],
...
]