Laravel: change log path to include user folder - laravel

I use Laravel 5.1 and I am trying to intercept the default logger configuration (Monolog) and save logs to a different path that includes user name.
The current logs are saved to storage/logs/laravel.log.
The wanted paths are as follows
Authenticated Users: storage/logs/[username]/[date]_[api_path].log
Other user logs can be saved under storage/logs/guest/[date]_[api_path].log
ServiceProvider Approach
Have a LogServiceProvider and where I can modify each request and set the path as wanted.
public function boot(Request $request)
{
$log = new Logger('View Logs');
$user = \Auth::User()->getName(); // ERROR - uninitialized
$path = 'storage/logs/'.$user.'/mylogfile.log'; // doesn't matter API path
$log ->pushHandler(new StreamHandler($path, Logger::INFO));
...
}
The problem with this approach, the Auth::user() seems to be uninitialized.
Why does this happen and how do I solve this?

You might need to wrap it in an if statement, such as:
if ( Auth::check() ) {
// Do auth stuff
}
else {
// Do unauth stuff
}
Does that solve your problem?

This is because Auth has not been initialized and it is too early to call.
There is example way to solve:
Create log initializer, and call it from middleware (or from wherever you want):
class UserLogger
{
public function init()
{
$logger = new Logger('order');
$currUserId = Auth::id();
$logPath = storage_path('logs/by_user/' . $currUserId . '/' . Carbon::now()->toDateString() . '.log');
$logger->pushHandler(new StreamHandler($logPath, Logger::INFO));
return $logger;
}
}
//app\Http\Middleware\Authenticate.php
class Authenticate
{
public function handle($request, Closure $next, $guard = null)
{
$userLogger = new UserLogger();
$logger = $userLogger->init();
$context = ['some context data' => 'data'];
$logger->info('custom user actions', $context);
//...
Laravel 8 version can be viewed here

Related

Can we use multiple second-level domains with multitenancy?

I have implemented the simplest example using the Spatie docs for multitenancy, that is working perfectly fine. Now, I intend to use multiple second-level domains for each tenant I have.
For example; I have 2 tenants company-a and company-b and they are being served at company-a.localhost and company-b.localhost, now what I want is that when I visit company-a.admin.localhost, it should tell me COMPANY-A ADMIN and If I visit company-a.employee.localhost, it should tell me COMPANY-A EMPLOYEE.
I have tried using subdomain on routes in RouteServiceProvider like the following:
Route::middleware('web')
->group(base_path('routes/security.php'));
Route::domain($this->baseDomain('admin'))
->middleware('web')
->name('admin.')
->group(base_path('routes/admin.php'));
Route::domain($this->baseDomain('employee'))
->middleware('web')
->name('employee.')
->group(base_path('routes/employee.php'));
private function baseDomain(string $subdomain = ''): string
{
if (strlen($subdomain) > 0) {
$subdomain = "{$subdomain}.";
}
return $subdomain . config('app.base_domain');
}
Without subdomain, it works fine, but the routes with second-level domain, it falls to base level domain route and does not get the current tenant.
What am I missing here? Is this even possible to implement.
Thankyou.
Take, for example, the route:
Route::domain('{subdomain}.example.com')
->get('/foo/{param1}/{param2}',function(Router $router) {
// do something with it
});
The binding fields would be ['subdomain', 'param1', 'param2'], and the compiled route would have it's regexes declared as
regex => "{^/foo/(?P<param1>[^/]++)/(?P<param2>[^/]++)$}sDu",
hostRegex => "{^(?P<subdomain>[^\.]++)\.example\.com$}sDiu"
Where ^(?P<subdomain>[^\.]++)\. will explicitly stop capturing when finding a dot, in this case the delimiter between groups.
However, these regexes are overridable by using the where method. You could declare the above route as
Route::domain('{subdomain}.example.com')
->get('/foo/{param1}/{param2}',function(Router $router) {
// do something with it
})->where('subdomain', '(.*)');
In the compiled route , the hostRegex would be now
hostRegex => "{^(?P<subdomain>(?:.*))\.example\.com$}sDiu"
Meaning it will capture anything preceding .example.com. If you requested company-a.admin.example.com, $subdomain would be company-a.admin.
You could also declare a route with two binding fields in its domain:
Route::domain('{subsubdomain}.{subdomain}.example.com')
->get('/foo/{param1}/{param2}',function(Router $router) {
// do something with it
});
Which might be more useful if you wanted subsubdomains to imply a hierarchy.
I have achieved this by using some checks, in RouteServiceProvider, I have not used the actual domain function on Route like we do normally i.e. Route::domain('foo.bar'). The reason was that, the Spatie package use a kind of middleware Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class which runs whenever we hit the domain with tenant comapny-a.localhost. And it gets the tenant from hostname i.e comapny-a.localhost.
public function findForRequest(Request $request):?Tenant
{
$host = $request->getHost();
return $this->getTenantModel()::whereDomain($host)->first();
}
In my RouteServiceProvide:
$this->routes(function () {
$class = 'security';
$middleware = 'web';
if (Str::contains(request()->getHost(), 'admin')) {
$class = 'admin';
} elseif (Str::contains(request()->getHost(), 'employee')) {
$class = 'employee';
} elseif (Str::contains(request()->getHost(), 'api')) {
$class = 'api';
$middleware = 'api';
}
Route::middleware($middleware)
->name("$class.")
->group(base_path("routes/${class}.php"));
});
As In my scenario, I had only these 2 kind of second-level domains and so, I just checked if this particular keyword exists in the hostname and choosing the file and middleware accordingly.
I also overrided the DomainTenantFinder class and in multitenancy.php config file:
public function findForRequest(Request $request): ?Tenant
{
$host = $request->getHost();
$host = str_replace('admin.', '', $host);
$host = str_replace('employee.', '', $host);
$host = str_replace('api.', '', $host);
$tenant = $this->getTenantModel()::whereDomain($host)->first();
if (empty($tenant)) {
abort(404);
}
return $tenant;
}
I have acheived the desired outcome, however, I have a security concern, specially in RouteServiceProvider logic. Thought??

Laravel - Recreate custom request with validation on the fly

I need to be able to recreate an $request variable, reach the validation of that request, and also beeing able to access route params (/foo/{user_id}) and query params (/foo?user_id), like this example.
Note! This is an silly example, i know that this specific problem can be resolved in a much simpler way, but my current enviriment is too complexi to explain, also it would be unnecessary
class RandomController {
// /foo/{field1}?field2
public function create(CreateRequest $request) {
if ($request->has('id') {
$new_request = recreateRequest($request->only('field1', 'field2', 'field3')); // field3 is on the body
return $this->update($new_request);
}
}
public function update(UpdateRequest $request) {
$request->field1; // /foo/{field1}
$request->field2; // /foo?field2
$request->field3; // {field3: }
}
}
Now I recreate the request passing it as an prop (CustomRequest::class) and using this following code, but it is unstable, sometimes it doesn't reach the request properties and I have to write $request->request->field1 instead of $request->field1
$created_request = new $new_request(
$query ?: $old_request->query->all(),
$request ?: $old_request->request->all(),
$attributes ?: $old_request->attributes->all(),
$cookies ?: $old_request->cookies->all(),
$files ?: $old_request->files->all(),
$server ?: $old_request->server->all()
);
you can create your own request object by artisan using the following command:
php artisan make:request RequestName
this should create a request file in your HTTP request folder and you can still use the global request variable $request so you can access all the parameters and use the validation.
and then just import it using
use Illuminate\Http\YourCustomRequest;
and replace the controller method argument with the new request you just imported like so
public function create(YourCustomRequest $myRequest) {
//do some action.
}
check the documentation here: https://laravel.com/docs/7.x/validation

Dynamic social app credentials in socialite package in Laravel

I'm trying to build a multi tenant app, where I have configured the database, the views folder, I know there must be some way out to configure the credentials of social app login for socialite. Well I tried few things to set it dynamically.
STEP 1
I created a class with the name of socialite in a separate folder and when the social login is called I'm implementing the following in my controller:
public function redirectSocialLogin()
{
$social = new SocialiteProvider();
$fb = $social->makeFacebookDriver();
return $fb->redirect();
}
and while callback I used following:
public function callbackSocialLogin($media)
{
$user = Socialite::driver($media)->user();
$data['name'] = $user->getName();
$data['email'] = $user->getEmail();
dd($data);
}
In my class I've following codes:
public function makeFacebookDriver()
{
$config['client_id'] = 'XXXXXXXXXXXXXXX';
$config['client_secret'] = 'XXXXXXXXXXXXXXX';
$config['redirect'] = 'http://XXXXXXXXXXXX/auth/facebook/callback';
return Socialite::buildProvider('\Laravel\Socialite\Two\FacebookProvider', $config);
}
It redirects perfectly to the social page but while getting a callback I'm getting an error, It again fetches the services.php file for configuration and doesn't get any.
STEP 2
I made a ServiceProvider under the name of SocialiteServiceProvider and extended the core SocialiteServiceProvider and placed the following codes:
protected function createFacebookDriver()
{
$config['client_id'] = 'XXXXXXXXXXXXXXX';
$config['client_secret'] = 'XXXXXXXXXXXXXXXXXXXX';
$config['redirect'] = 'http://XXXXXXXXXXX/auth/facebook/callback';
return $this->buildProvider(
'Laravel\Socialite\Two\FacebookProvider', $config
);
}
But again it throws back error which says driver is not setup. Help me out in this.
Thanks.
In your STEP 1, update the callback as below mentioned & try. $media is actually Request. So when initialising Socialite::driver($media) you are actually passing Request where you have to pass Facebook.
public function callbackSocialLogin(Request $request) {
$fbDriver = (new SocialiteProvider())->makeFacebookDriver();
$user = $fbDriver->user();
$data['name'] = $user->getName();
$data['email'] = $user->getEmail();
...
}

Phalcon: how to get controller and action name

I'm trying to build some authorization into my Phalcon-based app. In my bootstrap file I instantiate my Auth class (which extends Component), and run my authorize() function. Inside that function I get the dispatcher by calling $Dispatcher = $this->di->getShared('dispatcher').
That all seems to run fine. However, when I then call $Dispatcher->getControllerName(), it returns NULL.
How do I access the controller name?
Here is my bootstrap file:
$Debug = new \Phalcon\Debug();
$Debug->listen();
#try{
# Create a Dependency Injection container
$DI = new \Phalcon\DI\FactoryDefault();
# Load config
$Config = require '../app/config/config.php';
$DI->setShared('config',$Config);
# Register an autoloader
$Loader = new \Phalcon\Loader();
$Loader->registerDirs($Config->phalcon->load_dirs->toArray());
$Loader->registerNamespaces($Config->phalcon->namespaces->toArray());
$Loader->register();
# Initialize Session
$Session = new \Phalcon\Session\Adapter\Files();
$Session->start();
$DI->setShared('session',$Session);
# Set up the View component
$DI->set('view',function() use($Config){
$View = new \Phalcon\Mvc\View();
$View->setViewsDir($Config->dir->views_dir);
$View->registerEngines(['.phtml'=> function($View,$DI) use ($Config){
$Volt = new \Phalcon\Mvc\View\Engine\Volt($View,$DI);
$Volt->setOptions([ 'compiledPath' => $Config->dir->views_compile_dir,
'compileAlways' => $Config->app->views_compile_always
]);
return $Volt;
}
]);
$View->Config = $Config;
return $View;
});
# Check authorization
$Auth = new Auth($DI);
if($Auth->authorize()){
$DI->setShared('user',$Auth->getUser());
}
else{
$DI->get('view')->render('system','notallowed');
exit();
}
# Set up connection to database
$DI->set('db',function() use($Config){
return new \Phalcon\DB\Adapter\Pdo\Mysql([ 'host' => $Config->database->host,
'dbname' => $Config->database->database,
'username' => $Config->database->username,
'password' => $Config->database->password
]);
});
# Set up base URL
$DI->set('url',function() use($Config){
$URL = new \Phalcon\Mvc\Url();
$URL->setBaseUri('/'.basename($Config->dir->app_dir_web));
return $URL;
});
# Set up message flashing to use session instead of direct
$DI->set('flash',function(){
return new \Phalcon\Flash\Session();
});
# Handle the requested URL
$App = new \Phalcon\Mvc\Application($DI);
# Echo the output
echo $App->handle()->getContent();
/*
}
catch(\Phalcon\Exception $e){
echo 'Phalcon Exception: ',$e->getMessage();
}
*/
I think that untill you call $app->handle() the dispatcher won't be properly setup.
Maybe not a direct response, but I've manage to successfully implement authorization using Vokuro app as a example: https://github.com/phalcon/vokuro.
Your bootstrap looks ok should work.
I'm using this in bootstrap file :
/**
* Handle the request
*/
$application = new \Phalcon\Mvc\Application();
$application->setDI($di);
if (!empty($_SERVER['REQUEST_URI'])) {
$uriParts = explode('?', $_SERVER['REQUEST_URI']);
$uri = $uriParts[0];
} else {
$uri = '/';
}
echo $application->handle($uri)->getContent();
As you can see there is $uri parameter passed to $application->handle() function.
Inside controllers: $this->dispatcher->getControllerName() works fine.
I am using a Router object so the following works for me before the ->handle call
/** #var $router \Phalcon\Mvc\Router */
$router = require APPLICATION_PATH.'/routes/default.php';
$router->handle($url);
$router->setUriSource(\Phalcon\Mvc\Router::URI_SOURCE_SERVER_REQUEST_URI);
/** #var $matched \Phalcon\Mvc\Router\Route */
$matched = $router->getMatchedRoute();
$paths = $matched->getPaths();
echo 'controller : ',$paths['controller'],"<br />";
echo 'action : ',$paths['action'],"<br />";
This is an old thread but in case people still need it, I will show you two ways to properly handle authorization.
In both cases, you should check authorization in a "before middleware", not anywhere else.
Checking authorization directly in your front controller is a bit premature.
1. By retrieving the handler, as you asked
Here you perform authorization checks only if needed. It is a bit more code, but a bit more effective too because you don't have to even use the database if you don't need to.
$app = \Phalcon\Mvc\Micro;
$app->before(function() use ($app)
{
/**
* #var \Phalcon\Mvc\Controller $oHandler
*/
$oHandler = null;
$sAction = null;
$aHandlerArr = (array)$app->getActiveHandler();
if (!empty($aHandlerArr) && !empty($aHandlerArr[1]))
{
$oHandler = $aHandlerArr[0];
$sAction = $aHandlerArr[1];
}
if ($oHandler && $oHandler->isAuthRequired($sAction))
{
// Do auth, and send an error if failed
}
});
2. Without voodoo magic
The other way is to simply try authorization in your before middleware without checking if you need it. It is the same code as above, but without the handler retrieval.
$app = \Phalcon\Mvc\Micro;
$app->before(function() use ($app)
{
// Do auth ...
// Set member in your app, if auth succeeded.
// Then in your controller actions, simply check for the member in your app
});
XD
$this->router->getActionName()

Route resources set before auth not working in Laravel 4

I added this in routes.php, expected it will check the authentication session for the page, however it is not working.
Route::resource('ticket', 'TicketController', array('before' => 'auth') );
Then I go to the controller, work in another way. It's work.
class TicketController extends BaseController {
public function __construct()
{
$this->beforeFilter('auth');
}
May I know where can get more documentation regarding the Route::resource(), what type of argument it able to accept?
OK... I found the answer.
in
\vendor\laravel\framework\src\Illuminate\Routing\Router.php
public function resource($resource, $controller, array $options = array())
{
// If the resource name contains a slash, we will assume the developer wishes to
// register these resource routes with a prefix so we will set that up out of
// the box so they don't have to mess with it. Otherwise, we will continue.
if (str_contains($resource, '/'))
{
$this->prefixedResource($resource, $controller, $options);
return;
}
// We need to extract the base resource from the resource name. Nested resources
// are supported in the framework, but we need to know what name to use for a
// place-holder on the route wildcards, which should be the base resources.
$base = $this->getBaseResource($resource);
$defaults = $this->resourceDefaults;
foreach ($this->getResourceMethods($defaults, $options) as $method)
{
$this->{'addResource'.ucfirst($method)}($resource, $base, $controller);
}
}
protected function getResourceMethods($defaults, $options)
{
if (isset($options['only']))
{
return array_intersect($defaults, $options['only']);
}
elseif (isset($options['except']))
{
return array_diff($defaults, $options['except']);
}
return $defaults;
}
as you can see, it only accept only and except arguement only.
If you want to archive the same result in route.php, it can be done as below
Route::group(array('before'=>'auth'), function() {
Route::resource('ticket', 'TicketController');
});

Resources