You could just cache the id of all authenticated active users, every time they perform an action or load a page, you update the cache... and the cache expires after X minutes.
Find out all logged in users
I wish to be able to find all the logged in users in my application. I have had an idea about putting their id's into a database table(lets just call it "logged" for now) when they log in and then delete it when they log out, or their session expires(does this sound like the best way to code this? If not other suggestions would be great). I'm fairly certain I know where to insert my code for the login and logout and what methods to override in the illuminate/foundation/auth/authenticateUsers
For login
public function login(Request $request)
{
$this->validateLogin($request);
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return $this->sendLockoutResponse($request);
}
if ($this->attemptLogin($request)) {
//INSERT ID INTO DATABASE HERE
return $this->sendLoginResponse($request);
}
$this->incrementLoginAttempts($request);
return $this->sendFailedLoginResponse($request);
}
For logout
public function logout(Request $request)
{
//DELETE ID FROM DATABASE HERE
$this->guard()->logout();
$request->session()->flush();
$request->session()->regenerate();
return redirect('/');
}
Problem is I have no idea where to put my delete code for when session expires, meaning they would still appear logged in when in fact they are not. Any help would be great
You could also setup a table where you insert id of all the active users (which you update whenever a user makes a request), and delete from the said table if the session time from the last request expired (using created_at and updated_at timestamps).
Nice use created_at and updated_at timestamps
@ Mittensoff Okay so I get I need to detect the refresh of each users session when they perform an action/load a page. That makes sense, but how would I go about doing that?
@psychomantis101 suggested solution by @Mittensoff is good if in your system will working be some 10-30 users. In case if you have some 100 active users and each click add SQL to update user record (alive) + check for other users status SQL DB will die or your system will work slow.
@TomyLimon I agree, inserts can be slow then. Though he could counter it by using a NoSQL DB, like MongoDB just for those.
Every time a user access your page or submits a form, a request is sent to your Laravel application. Your request first check with middleware then go check routes for their designated controller.
What that means for you is: in every controller check for id of the user that makes a request for the page display or a form submit. Do that in an if command in the beggining of a controller or wherever before the return command. In that if make an update/insert into the appropriate table.
Though after mentioning what TomyLimon said, I'd suggest adding a second DB with NoSQL, because they handle many inserts/updates a lot better then relative DBs (e.g. Facebook/Twitter use it).
@Mittensoff. Since it would have to be in every controller, would in not be better to put that code in controller.php itself? Since all other controllers are an extension of it. I could put the code into middleware and shove it into the controller.php construct right?
If you put it in middleware it'll get 'executed' before it reaches controllers so it's probably the best solution.
<?php
namespace App\Http\Middleware;
use Closure;
class YourMiddleware
{
public function handle($request, Closure $next)
{
// Check here which user made the request
// Update the DB or Redis or whatever you choose
return $next($request); // Continue to the controller action
}
}
Okay so I think I have got this working with Redis. Any advice to improve would be great. I have put it on stackoverflow as I want to be sure to get this right, here is the link.
Okay I found a bug in the code that I posted to stackoverflow. Basically when a person logs into their account on 2 different machines, and then logs out of one of them, it would appear as if he or she was logged out. I have (mostly) fixed it by adding browser details to the namespace. Thought I would post everything I did here, just in case somebody else finds this thread with the same problem as me. Or if anyone can see something to improve it. If you are wondering why I didn't use lists/sets, it's because you can't set expire dates on individual members, only the whole list.
Step 1: I used Laravel's built in authentication, running
php artisan make:auth
Step 2: I installed Predis, running
composer require predis/predis
Step 3: In LoginController.php I overrode 2 methods found in Illuminate\Foundation\Auth\AuthenticatesUsers
The 2 methods are "authenticated" (which was actually empty) and "logout". Once I had pasted them into LoginController.php, I added the Redis code into them, see below.
// Overriding the authenticated method from Illuminate\Foundation\Auth\AuthenticatesUsers
protected function authenticated(Request $request, $user)
{
// Building namespace for Redis
$id = $user->id;
$browser = $request->server('HTTP_USER_AGENT');
$namespace = 'users:'.$id.$browser;
// Getting the expiration from the session config file. Converting from minutes to seconds.
$expire = config('session.lifetime') * 60;
// Setting redis using id as value
Redis::SET($namespace,$id);
Redis::EXPIRE($namespace,$expire);
}
// Overriding the logout method from Illuminate\Foundation\Auth\AuthenticatesUsers
public function logout(Request $request)
{
// Building namespace for Redis
$id = Auth::user()->id;
$browser = $request->server('HTTP_USER_AGENT');
$namespace = 'users:'.$id.$browser;
// Deleting user from redis database when they log out
Redis::DEL($namespace);
$this->guard()->logout();
$request->session()->flush();
$request->session()->regenerate();
return redirect('/');
}
Of course I had to add this at the top of the controller
use Auth;
use Illuminate\Support\Facades\Redis;
Next I wrote some middleware for refreshing the expire date of the reddis key. I named it "RefreshRedis", run command.
php artisan make:middleware RefreshRedis
The code for the middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Auth;
use Illuminate\Support\Facades\Redis;
class RefreshRedis
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
// Checking if http request is coming from logged in user
if(Auth::check()){
// Creating namespace for Redis
$id = Auth::user()->id;
$browser = $request->server('HTTP_USER_AGENT');
$namespace = 'users:'.$id.$browser;
// Refreshing the expiration of logged user
$expire = config('session.lifetime') * 60;
Redis::EXPIRE($namespace,$expire);
}
return $next($request);
}
}
I registered the middleware in Kernal.php. In the $middlewareGroups. This is the bit I'm not 100% sure on, it seems to work, but if somebody thinks it's better somewhere else please say.
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\RefreshRedis::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
I decided to create a helper class for getting the results. In app/http I created a directory called 'Custom'. Inside the new 'Custom' directory I created a php class called 'Helper'. The 'loggedUsers' method inside the class is a modified piece of code that I found from here http://stackoverflow.com/questions/35477172/laravel-and-redis-scan
<?php
namespace App\Http\Custom;
use Illuminate\Support\Facades\Redis;
use App\User;
class Helper
{
public static function loggedUsers($cursor=null, $allResults=array())
{
// Zero means full iteration
if ($cursor==="0"){
// Get rid of duplicated values caused by redis scan limitations.
$allResults = array_unique($allResults);
// Setting users array
$users = array ();
// Looping through all results. Inserting each logged user into array.
foreach($allResults as $result){
$users[] = User::where('id',Redis::Get($result))->first();
}
// Removing duplicate items. (If user has logged in using more than one machine)
$users = array_unique($users);
return $users;
}
// No $cursor means init
if ($cursor===null){
$cursor = "0";
}
// The call
$result = Redis::scan($cursor, 'match', 'users:*');
// Append results to array
$allResults = array_merge($allResults, $result[1]);
// Recursive call until cursor is 0
return self::loggedUsers($result[0], $allResults);
}
}
I set the aliases for helper in config/app.php
'aliases' => [
'Helper' => App\Http\Custom\Helper::class,
Then whenever I need to find all logged users , I call the helper class.
<?php
namespace App\Widgets;
use Arrilot\Widgets\AbstractWidget;
use Helper;
class Logged extends AbstractWidget
{
/**
* Treat this method as a controller action.
* Return view() or other content to display.
*/
public function run()
{
//Find all logged users id's from redis
$users = Helper::loggedUsers();
return view('widgets.logged',compact('users'));
}
}
Then in the view, I would do something like this.
@foreach($users as $user)
<div><strong>{{$user->name}}</strong></div>
@endforeach
I use a middleware that fires a queued event. Also keep in mind getting auth() also fires a query. So you probably are firing at least one query per page load right away.
Hmm, good point about the auth()? Might not matter for my app, but can you explain more how you used a queued event for finding logged users?
You could store your session data in a database and just query the session table.
Thanks so much, psychomantis101, you have saved me a lot of research and time. I used your code above step by step and it works a treat. tested it with a user logged in on different browsers too, and it is working very well.
I didn't even know where to start with this feature, but I have it up and running in 15 minutes...now that's crazy!
Just add the trait code to:
app/Http/Controllers/Auth/loginController.php
As you know, If a controller uses a trait like AuthenticatedUsers when you create the same method name as the trait you'll be able to override it.
Else, when you run composer update the Illuminate\Foundation\Auth\AuthenticatesUsers file is reverted back to default and your code is removed.
I did a different approach. I configure to use database sessions (mysql).
php artisan session:table
php artisan migrate
Create appropriate model
pho artisan make:model sessions
Since the last_activity on sessions table is stored as epoch (Unix timestamp since 1970-1-1), we can query all the active sessions that are bigger than the current time stamp minus the session expire time.
<?php
// Controller code
use App\sessions;
public function liveSessions(){
// Get time session life time from config.
$time = time() - (config('session.lifetime')*60);
// Total login users (user can be log on 2 devices will show once.)
$totalActiveUsers = sessions::where('last_activity','>=', $time)->
count(DB::raw('DISTINCT user_id'));
// Total active sessions
$totalActiveUsers = sessions::where('last_activity','>=', $time)->
get();
}
Hope this help anyone.
I did this in another way which uses Redis for performance.
Someone can refer this : https://laracasts.com/discuss/channels/laravel/how-to-retrieve-all-alive-sessions-including-another-uses-session
While reading this i realized, if we are using sanctum auth, the updated_at time stamp could be a quick win if this doesn't need to be perfect. Otherwise, native Auth events &/or middleware.
DB::table('personal_access_tokens')->where('updated_at', '>=', now())->subHours(1)->count()
Please or to participate in this conversation.