mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
Track last login date and IP
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddLoginFieldsToUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* The name of the table for the migration.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tableName = 'users';
|
||||
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table($this->tableName, function (Blueprint $table) {
|
||||
if (!Schema::hasColumn($this->tableName, 'last_login_at')) {
|
||||
$table->datetime('last_login_at')->nullable();
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn($this->tableName, 'last_login_ip')) {
|
||||
$table->string('last_login_ip')->nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table($this->tableName, function (Blueprint $table) {
|
||||
if (!Schema::hasColumn($this->tableName, 'last_login_at')) {
|
||||
$table->dropColumn('last_login_at');
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn($this->tableName, 'last_login_ip')) {
|
||||
$table->dropColumn('last_login_ip');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+142953
-1
File diff suppressed because one or more lines are too long
Vendored
+1804
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"/js/app.js": "/js/app.js?id=f6e8984136ad30e3b70c",
|
||||
"/css/app.css": "/css/app.css?id=09dc8347908c0e034608"
|
||||
"/js/app.js": "/js/app.js?id=a996ac7f2a8afb2ad2e7",
|
||||
"/css/app.css": "/css/app.css?id=fe89a3abc3e5090bfdc5"
|
||||
}
|
||||
|
||||
@@ -22,10 +22,8 @@ return [
|
||||
|
||||
'permissions' => 'Permissions',
|
||||
|
||||
'signed_in_as' => 'Signed in as',
|
||||
'created_at' => 'Created',
|
||||
|
||||
'account_settings' => 'Account settings',
|
||||
|
||||
'user_account' => ':name\'s Account',
|
||||
'updated_at' => 'Last updated',
|
||||
|
||||
];
|
||||
|
||||
@@ -14,6 +14,8 @@ return [
|
||||
|
||||
'login' => 'Sign in to :name',
|
||||
|
||||
'signed_in_as' => 'Signed in as',
|
||||
|
||||
'logout' => 'You have been logged out successfully.',
|
||||
|
||||
];
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Permissions Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used for user specific
|
||||
| messages that we need to display to the user.
|
||||
|
|
||||
*/
|
||||
|
||||
'super_admin' => 'Super admin',
|
||||
|
||||
'super_admin_info' => 'Super admins have access to all of :name.',
|
||||
|
||||
];
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used for user specific
|
||||
| messages that we need to display to the user.
|
||||
|
|
||||
*/
|
||||
|
||||
'settings' => 'Account settings',
|
||||
|
||||
'account' => ':name\'s Account',
|
||||
|
||||
'updated' => '**:name** updated successfully.',
|
||||
|
||||
'last_login_at' => 'Last login',
|
||||
|
||||
'last_login_never' => 'never',
|
||||
|
||||
'last_login_ip' => 'Last login IP',
|
||||
|
||||
];
|
||||
@@ -1,4 +1,4 @@
|
||||
<div x-data="{ tab: '{{ $tab }}' }" {{ $attributes->merge(['class' => 'bg-white overflow-hidden shadow rounded-lg']) }}>
|
||||
<div x-data="{ tab: '{{ $tab }}' }" {{ $attributes->merge(['class' => 'bg-white overflow-hidden shadow rounded']) }}>
|
||||
<div role="tablist" class="border-b border-gray-200 bg-gray-50 flex">
|
||||
@foreach($tabs as $value => $label)
|
||||
<button role="tab" aria-controls="{{ $value }}-tab" :tabindex="tab === '{{ $value }}' ? 0 : -1" :aria-selected="tab === '{{ $value }}' ? 'true' : 'false'" class="border-r border-gray-200 px-4 py-3 text-gray-500 text-sm leading-tight -mb-px transition duration-200 ease-in-out" :class="{ 'text-current bg-white': tab === '{{ $value }}' }" @click.prevent="tab = '{{ $value }}'">
|
||||
|
||||
+26
-4
@@ -1,6 +1,6 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 md:gap-4 lg:gap-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-7 gap-4 lg:gap-8">
|
||||
|
||||
<form wire:submit.prevent="update" class="col-span-2">
|
||||
<form wire:submit.prevent="update" class="md:col-span-5">
|
||||
|
||||
<x-filament-tabs tab="account" :tabs="['account' => 'Account', 'permissions' => 'Permissions']">
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
<x-filament-tab id="permissions">
|
||||
|
||||
<x-filament-input-group>
|
||||
<x-filament-checkbox name="is_super_admin" label="Super admin?" wire:model="is_super_admin" />
|
||||
<x-filament-checkbox name="is_super_admin" label="filament::permissions.super_admin" wire:model="is_super_admin" />
|
||||
<x-slot name="info">
|
||||
Super admins have access to all of {{ config('app.name') }}.
|
||||
{{ __('filament::permissions.super_admin_info', ['name' => config('app.name')]) }}
|
||||
</x-slot>
|
||||
</x-filament-input-group>
|
||||
|
||||
@@ -44,4 +44,26 @@
|
||||
|
||||
</form>
|
||||
|
||||
<x-filament-well class="md:col-span-2">
|
||||
|
||||
<dl class="grid grid-cols-2 gap-2 text-xs leading-tight">
|
||||
|
||||
<dt class="text-gray-400">{{ __('filament::admin.created_at') }}</dt>
|
||||
<dd class="text-right">{{ $user->created_at->fromNow() }}</dd>
|
||||
|
||||
<dt class="text-gray-400">{{ __('filament::admin.updated_at') }}</dt>
|
||||
<dd class="text-right">{{ $user->updated_at->fromNow() }}</dd>
|
||||
|
||||
<dt class="text-gray-400">{{ __('filament::user.last_login_at') }}</dt>
|
||||
<dd class="text-right">{{ $user->last_login_at ? $user->last_login_at->fromNow() : __('filament::user.last_login_never') }}</dd>
|
||||
|
||||
@if ($user->last_login_ip)
|
||||
<dt class="text-gray-400">{{ __('filament::user.last_login_ip') }}</dt>
|
||||
<dd class="text-right">{{ $user->last_login_ip }}</dd>
|
||||
@endif
|
||||
|
||||
</dl>
|
||||
|
||||
</x-filament-well>
|
||||
|
||||
</div>
|
||||
@@ -1 +0,0 @@
|
||||
<span @if ($classes) class="{{ $classes }}" @endif>{{ $email }}</span>
|
||||
@@ -1 +0,0 @@
|
||||
<span @if ($classes) class="{{ $classes }}" @endif>{{ $name }}</span>
|
||||
@@ -4,7 +4,7 @@
|
||||
<li wire:key="{{ $user->id }}" class="flex">
|
||||
<a class="flex-grow p-4 bg-white shadow-sm rounded-md flex items-center" href="{{ route('filament.admin.users.edit', ['user' => $user->id]) }}">
|
||||
@livewire('filament::user-avatar', [
|
||||
'user' => $user,
|
||||
'userId' => $user->id,
|
||||
'size' => 96,
|
||||
'classes' => 'h-12 w-12 rounded-full'
|
||||
], key($user->id))
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<div @click.away="open = false" class="relative" x-data="{ open: false }">
|
||||
<button @click="open = !open" class="flex items-center">
|
||||
@livewire('filament::user-avatar', [
|
||||
'user' => auth()->user(),
|
||||
'userId' => auth()->user()->id,
|
||||
'size' => 36,
|
||||
'classes' => 'h-9 w-9 rounded-full',
|
||||
])
|
||||
@livewire('filament::user-name', [
|
||||
'name' => auth()->user()->name,
|
||||
@livewire('filament::auth-user-attribute', [
|
||||
'attribute' => 'name',
|
||||
'classes' => 'ml-3 text-sm leading-5 font-medium text-white',
|
||||
])
|
||||
</button>
|
||||
@@ -21,16 +21,16 @@
|
||||
<div class="rounded-md bg-white shadow-xs">
|
||||
<p class="px-4 py-3">
|
||||
<span class="block text-sm leading-5">
|
||||
{{ __('filament::admin.signed_in_as') }}
|
||||
{{ __('filament::auth.signed_in_as') }}
|
||||
</span>
|
||||
@livewire('filament::user-email', [
|
||||
'email' => auth()->user()->email,
|
||||
@livewire('filament::auth-user-attribute', [
|
||||
'attribute' => 'email',
|
||||
'classes' => 'text-sm leading-5 font-medium text-gray-900',
|
||||
])
|
||||
</p>
|
||||
<div class="border-t border-gray-100"></div>
|
||||
<div class="py-1">
|
||||
<a href="{{ route('filament.admin.users.edit', ['user' => auth()->user()->id]) }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ __('filament::admin.account_settings') }}</a>
|
||||
<a href="{{ route('filament.admin.users.edit', ['user' => auth()->user()->id]) }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ __('filament::user.settings') }}</a>
|
||||
</div>
|
||||
<div class="border-t border-gray-100"></div>
|
||||
<div class="py-1">
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
@section('title', $title)
|
||||
|
||||
@section('main')
|
||||
@livewire('filament::user-edit-form', ['user' => $user])
|
||||
@livewire('filament::user-edit', ['user' => $user])
|
||||
@endsection
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Http\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class Well extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represent the component.
|
||||
*
|
||||
* @return \Illuminate\View\View|string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
return <<<'blade'
|
||||
<div {{ $attributes->merge(['class' => 'border border-gray-200 bg-gray-50 overflow-hidden rounded']) }}>
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
blade;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ namespace Filament\Http\Controllers\Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
use Filament\Http\Controllers\Controller;
|
||||
|
||||
class LoginController extends Controller
|
||||
@@ -67,4 +66,19 @@ class LoginController extends Controller
|
||||
'message' => __('filament::auth.logout'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has been authenticated.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param mixed $user
|
||||
* @return mixed
|
||||
*/
|
||||
protected function authenticated(Request $request, $user)
|
||||
{
|
||||
$user->timestamps = false;
|
||||
$user->last_login_at = now();
|
||||
$user->last_login_ip = $request->ip();
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class UserController extends Controller
|
||||
|
||||
$this->authorize('edit', $user);
|
||||
|
||||
$title = __('filament::admin.user_account', ['name' => $user->name]);
|
||||
$title = __('filament::user.account', ['name' => $user->name]);
|
||||
|
||||
return view('filament::users.edit', compact('title', 'user'));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Http\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class AuthUserAttribute extends Component
|
||||
{
|
||||
public $attribute;
|
||||
public $classes;
|
||||
|
||||
protected $listeners = ['authUserUpdated' => 'render'];
|
||||
|
||||
public function mount(string $attribute, string $classes = '')
|
||||
{
|
||||
$this->attribute = $attribute;
|
||||
$this->classes = $classes;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return <<<'blade'
|
||||
<span class="{{ $classes }}">{{ auth()->user()->{$this->attribute} }}</span>
|
||||
blade;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Filament\Http\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use Filament\Contracts\User as UserContract;
|
||||
|
||||
class UserAvatar extends Component
|
||||
{
|
||||
@@ -12,15 +13,29 @@ class UserAvatar extends Component
|
||||
|
||||
protected $listeners = ['userUpdated' => 'render'];
|
||||
|
||||
public function mount($user, int $size, string $classes = '')
|
||||
public function mount(int $userId, int $size, string $classes = '')
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->user = $this->getUser($userId);
|
||||
$this->size = $size;
|
||||
$this->classes = $classes;
|
||||
}
|
||||
|
||||
public function updateUser(int $id)
|
||||
{
|
||||
if ($this->user->id === $id) {
|
||||
$this->user = $this->getUser($id);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('filament::livewire.user-avatar');
|
||||
}
|
||||
|
||||
protected function getUser(int $userId)
|
||||
{
|
||||
$userClass = app(UserContract::class);
|
||||
|
||||
return $userClass::findOrFail($userId);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use Livewire\Component;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Spatie\Permission\Contracts\Role as RoleContract;
|
||||
|
||||
class UserEditForm extends Component
|
||||
class UserEdit extends Component
|
||||
{
|
||||
public $user;
|
||||
public $name;
|
||||
@@ -39,21 +39,27 @@ class UserEditForm extends Component
|
||||
|
||||
$this->user->update($validatedData);
|
||||
|
||||
$this->emit('notification.notify', [
|
||||
'type' => 'success',
|
||||
'message' => "User {$this->name} updated successfully.",
|
||||
]);
|
||||
if (isset($validatedData['user_roles'])) {
|
||||
$this->user->syncRoles($validatedData['user_roles']);
|
||||
}
|
||||
|
||||
$this->emit('userUpdated', $this->user->id);
|
||||
|
||||
if (auth()->user()->id === $this->user->id) {
|
||||
$this->emit('userUpdated', $this->user);
|
||||
$this->emit('authUserUpdated');
|
||||
}
|
||||
|
||||
$this->emit('notification.notify', [
|
||||
'type' => 'success',
|
||||
'message' => __('filament::user.updated', ['name' => $this->name]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$roleClass = app(RoleContract::class);
|
||||
|
||||
return view('filament::livewire.user-edit-form', [
|
||||
return view('filament::livewire.user-edit', [
|
||||
'roles' => $roleClass::all(),
|
||||
]);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Http\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class UserEmail extends Component
|
||||
{
|
||||
public $email;
|
||||
public $classes;
|
||||
|
||||
protected $listeners = ['userUpdated' => 'updateEmail'];
|
||||
|
||||
public function mount(string $email, string $classes = '')
|
||||
{
|
||||
$this->email = $email;
|
||||
$this->classes = $classes;
|
||||
}
|
||||
|
||||
public function updateEmail(array $user) {
|
||||
$this->email = collect($user)->get('email', $this->email);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('filament::livewire.user-email');
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Filament\Http\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class UserName extends Component
|
||||
{
|
||||
public $name;
|
||||
public $classes;
|
||||
|
||||
protected $listeners = ['userUpdated' => 'updateName'];
|
||||
|
||||
public function mount(string $name, string $classes = '')
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->classes = $classes;
|
||||
}
|
||||
|
||||
public function updateName(array $user) {
|
||||
$this->name = collect($user)->get('name', $this->name);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('filament::livewire.user-name');
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class Users extends Component
|
||||
$userClass = app(UserContract::class);
|
||||
|
||||
return view('filament::livewire.users', [
|
||||
'users' => $userClass::paginate(9),
|
||||
'users' => $userClass::paginate(12),
|
||||
]);
|
||||
}
|
||||
}
|
||||
+21
-12
@@ -15,8 +15,17 @@ trait FilamentUser
|
||||
*/
|
||||
public function initializeFilamentUser()
|
||||
{
|
||||
$this->mergeFillable(['is_super_admin', 'avatar']);
|
||||
$this->mergeCasts(['is_super_admin' => 'boolean']);
|
||||
$this->mergeFillable([
|
||||
'is_super_admin',
|
||||
'avatar',
|
||||
'last_login_at',
|
||||
'last_login_ip',
|
||||
]);
|
||||
|
||||
$this->mergeCasts([
|
||||
'is_super_admin' => 'boolean',
|
||||
'last_login_at' => 'datetime',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,16 +39,6 @@ trait FilamentUser
|
||||
return Gravatar::src($this->email, (int) $size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge fillable attributes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function mergeFillable(array $fillable)
|
||||
{
|
||||
$this->fillable(collect($this->fillable)->merge($fillable)->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the password reset notification.
|
||||
*
|
||||
@@ -61,4 +60,14 @@ trait FilamentUser
|
||||
{
|
||||
$this->attributes['password'] = Hash::make($pass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge fillable attributes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function mergeFillable(array $fillable)
|
||||
{
|
||||
$this->fillable(collect($this->fillable)->merge($fillable)->all());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user