mirror of
https://github.com/Bubka/2FAuth.git
synced 2024-11-24 17:23:54 +01:00
Add authentications log
This commit is contained in:
parent
1bc55f5535
commit
a6745c28a6
7919
_ide_helper.php
7919
_ide_helper.php
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,7 @@
|
||||
|
||||
use App\Api\v1\Requests\UserManagerPromoteRequest;
|
||||
use App\Api\v1\Requests\UserManagerStoreRequest;
|
||||
use App\Api\v1\Resources\UserAuthentication;
|
||||
use App\Api\v1\Resources\UserManagerResource;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
@ -193,8 +194,7 @@ public function promote(UserManagerPromoteRequest $request, User $user)
|
||||
{
|
||||
$this->authorize('promote', $user);
|
||||
|
||||
if ($user->promoteToAdministrator($request->validated('is_admin')))
|
||||
{
|
||||
if ($user->promoteToAdministrator($request->validated('is_admin'))) {
|
||||
$user->save();
|
||||
Log::info(sprintf('User ID #%s set is_admin=%s for User ID #%s', $request->user()->id, $user->isAdministrator(), $user->id));
|
||||
|
||||
@ -206,6 +206,24 @@ public function promote(UserManagerPromoteRequest $request, User $user)
|
||||
], 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's authentication logs
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function authentications(Request $request, User $user)
|
||||
{
|
||||
$this->authorize('view', $user);
|
||||
|
||||
$validated = $this->validate($request, [
|
||||
'limit' => 'sometimes|numeric',
|
||||
]);
|
||||
|
||||
$authentications = $request->has('limit') ? $user->authentications->take($validated['limit']) : $user->authentications;
|
||||
|
||||
return UserAuthentication::collection($authentications);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the broker to be used during password reset.
|
||||
*
|
||||
|
63
app/Api/v1/Resources/UserAuthentication.php
Normal file
63
app/Api/v1/Resources/UserAuthentication.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Api\v1\Resources;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Jenssegers\Agent\Agent;
|
||||
|
||||
/**
|
||||
* @property mixed $id
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property string $oauth_provider
|
||||
* @property \Illuminate\Support\Collection<array-key, mixed> $preferences
|
||||
* @property string $is_admin
|
||||
*/
|
||||
class UserAuthentication extends JsonResource
|
||||
{
|
||||
/**
|
||||
* A user agent parser instance.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $agent;
|
||||
|
||||
/**
|
||||
* Create a new resource instance.
|
||||
*
|
||||
* @param mixed $resource
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($resource)
|
||||
{
|
||||
$this->agent = new Agent();
|
||||
$this->agent->setUserAgent($resource->user_agent);
|
||||
|
||||
parent::__construct($resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'ip_address' => $this->ip_address,
|
||||
'user_agent' => $this->user_agent,
|
||||
'browser' => $this->agent->browser(),
|
||||
'platform' => $this->agent->platform(),
|
||||
'device' => $this->agent->deviceType(),
|
||||
'login_at' => Carbon::parse($this->login_at)->toDayDateTimeString(),
|
||||
'login_successful' => $this->login_successful,
|
||||
'duration' => $this->logout_at
|
||||
? Carbon::parse($this->logout_at)->diffForHumans(Carbon::parse($this->login_at), ['syntax' => CarbonInterface::DIFF_ABSOLUTE])
|
||||
: null,
|
||||
];
|
||||
}
|
||||
}
|
@ -70,6 +70,7 @@ protected function flushDB() : void
|
||||
DB::table('groups')->delete();
|
||||
DB::table('users')->delete();
|
||||
DB::table('options')->delete();
|
||||
DB::table(config('authentication-log.table_name'))->delete();
|
||||
|
||||
$this->line('Database cleaned');
|
||||
}
|
||||
|
@ -6,7 +6,6 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\UserDeleteRequest;
|
||||
use App\Http\Requests\UserUpdateRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
@ -12,6 +12,7 @@
|
||||
use Illuminate\Support\Str;
|
||||
use Laragear\WebAuthn\WebAuthnAuthentication;
|
||||
use Laravel\Passport\HasApiTokens;
|
||||
use Rappasoft\LaravelAuthenticationLog\Traits\AuthenticationLoggable;
|
||||
|
||||
/**
|
||||
* App\Models\User
|
||||
@ -42,6 +43,7 @@
|
||||
*/
|
||||
class User extends Authenticatable implements WebAuthnAuthenticatable
|
||||
{
|
||||
use AuthenticationLoggable;
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
use WebAuthnAuthentication, WebAuthnManageCredentials;
|
||||
|
||||
@ -81,7 +83,7 @@ class User extends Authenticatable implements WebAuthnAuthenticatable
|
||||
* These are extra user-defined events observers may subscribe to.
|
||||
*/
|
||||
protected $observables = [
|
||||
'demoting'
|
||||
'demoting',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -108,7 +110,7 @@ public function isAdministrator()
|
||||
/**
|
||||
* Grant administrator permissions to the user.
|
||||
*/
|
||||
public function promoteToAdministrator(bool $promote = true): bool
|
||||
public function promoteToAdministrator(bool $promote = true) : bool
|
||||
{
|
||||
if ($promote == false && $this->fireModelEvent('demoting') === false) {
|
||||
return false;
|
||||
|
49
app/Notifications/SignedInWithNewDevice.php
Normal file
49
app/Notifications/SignedInWithNewDevice.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Jenssegers\Agent\Agent;
|
||||
use Rappasoft\LaravelAuthenticationLog\Models\AuthenticationLog;
|
||||
|
||||
class SignedInWithNewDevice extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public AuthenticationLog $authenticationLog;
|
||||
|
||||
/**
|
||||
* A user agent parser instance.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $agent;
|
||||
|
||||
public function __construct(AuthenticationLog $authenticationLog)
|
||||
{
|
||||
$this->authenticationLog = $authenticationLog;
|
||||
$this->agent = new Agent();
|
||||
$this->agent->setUserAgent($authenticationLog->user_agent);
|
||||
}
|
||||
|
||||
public function via($notifiable)
|
||||
{
|
||||
return $notifiable->notifyAuthenticationLogVia();
|
||||
}
|
||||
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
return (new MailMessage())
|
||||
->subject(__('notifications.new_device.subject'))
|
||||
->markdown('emails.newDevice', [
|
||||
'account' => $notifiable,
|
||||
'time' => $this->authenticationLog->login_at,
|
||||
'ipAddress' => $this->authenticationLog->ip_address,
|
||||
'browser' => $this->agent->browser(),
|
||||
'platform' => $this->agent->platform(),
|
||||
]);
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@
|
||||
"google/protobuf": "^3.21",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"jackiedo/dotenv-editor": "^2.1",
|
||||
"jenssegers/agent": "^2.6",
|
||||
"khanamiryan/qrcode-detector-decoder": "^2.0.2",
|
||||
"laragear/webauthn": "^2.0",
|
||||
"laravel/framework": "^10.10",
|
||||
@ -34,6 +35,7 @@
|
||||
"laravel/tinker": "^2.8",
|
||||
"laravel/ui": "^4.2",
|
||||
"paragonie/constant_time_encoding": "^2.6",
|
||||
"rappasoft/laravel-authentication-log": "^4.0",
|
||||
"socialiteproviders/manager": "^4.4",
|
||||
"spatie/eloquent-sortable": "^4.0.1",
|
||||
"spomky-labs/otphp": "^11.0"
|
||||
@ -101,4 +103,4 @@
|
||||
"vendor/bin/phpunit --coverage-html tests/Coverage/"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
270
composer.lock
generated
270
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "bce4feb20f25403dd63ac68e2e56f84a",
|
||||
"content-hash": "360f86c3dabb2c352bac0d34bb749d1f",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@ -1859,6 +1859,141 @@
|
||||
},
|
||||
"time": "2022-03-07T20:28:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "jaybizzle/crawler-detect",
|
||||
"version": "v1.2.117",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/JayBizzle/Crawler-Detect.git",
|
||||
"reference": "6785557f03d0fa9e2205352ebae9a12a4484cc8e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/6785557f03d0fa9e2205352ebae9a12a4484cc8e",
|
||||
"reference": "6785557f03d0fa9e2205352ebae9a12a4484cc8e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8|^5.5|^6.5|^9.4"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Jaybizzle\\CrawlerDetect\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mark Beech",
|
||||
"email": "m@rkbee.ch",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "CrawlerDetect is a PHP class for detecting bots/crawlers/spiders via the user agent",
|
||||
"homepage": "https://github.com/JayBizzle/Crawler-Detect/",
|
||||
"keywords": [
|
||||
"crawler",
|
||||
"crawler detect",
|
||||
"crawler detector",
|
||||
"crawlerdetect",
|
||||
"php crawler detect"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/JayBizzle/Crawler-Detect/issues",
|
||||
"source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.117"
|
||||
},
|
||||
"time": "2024-03-19T22:51:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "jenssegers/agent",
|
||||
"version": "v2.6.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/jenssegers/agent.git",
|
||||
"reference": "daa11c43729510b3700bc34d414664966b03bffe"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/jenssegers/agent/zipball/daa11c43729510b3700bc34d414664966b03bffe",
|
||||
"reference": "daa11c43729510b3700bc34d414664966b03bffe",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"jaybizzle/crawler-detect": "^1.2",
|
||||
"mobiledetect/mobiledetectlib": "^2.7.6",
|
||||
"php": ">=5.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-coveralls/php-coveralls": "^2.1",
|
||||
"phpunit/phpunit": "^5.0|^6.0|^7.0"
|
||||
},
|
||||
"suggest": {
|
||||
"illuminate/support": "Required for laravel service providers"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Jenssegers\\Agent\\AgentServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"Agent": "Jenssegers\\Agent\\Facades\\Agent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Jenssegers\\Agent\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jens Segers",
|
||||
"homepage": "https://jenssegers.com"
|
||||
}
|
||||
],
|
||||
"description": "Desktop/mobile user agent parser with support for Laravel, based on Mobiledetect",
|
||||
"homepage": "https://github.com/jenssegers/agent",
|
||||
"keywords": [
|
||||
"Agent",
|
||||
"browser",
|
||||
"desktop",
|
||||
"laravel",
|
||||
"mobile",
|
||||
"platform",
|
||||
"user agent",
|
||||
"useragent"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/jenssegers/agent/issues",
|
||||
"source": "https://github.com/jenssegers/agent/tree/v2.6.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/jenssegers",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/jenssegers/agent",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-06-13T08:05:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "khanamiryan/qrcode-detector-decoder",
|
||||
"version": "2.0.2",
|
||||
@ -3593,6 +3728,68 @@
|
||||
],
|
||||
"time": "2024-03-23T07:42:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mobiledetect/mobiledetectlib",
|
||||
"version": "2.8.45",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/serbanghita/Mobile-Detect.git",
|
||||
"reference": "96aaebcf4f50d3d2692ab81d2c5132e425bca266"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/96aaebcf4f50d3d2692ab81d2c5132e425bca266",
|
||||
"reference": "96aaebcf4f50d3d2692ab81d2c5132e425bca266",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.8.36"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Detection": "namespaced/"
|
||||
},
|
||||
"classmap": [
|
||||
"Mobile_Detect.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Serban Ghita",
|
||||
"email": "serbanghita@gmail.com",
|
||||
"homepage": "http://mobiledetect.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Mobile_Detect is a lightweight PHP class for detecting mobile devices. It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.",
|
||||
"homepage": "https://github.com/serbanghita/Mobile-Detect",
|
||||
"keywords": [
|
||||
"detect mobile devices",
|
||||
"mobile",
|
||||
"mobile detect",
|
||||
"mobile detector",
|
||||
"php mobile detect"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/serbanghita/Mobile-Detect/issues",
|
||||
"source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.45"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/serbanghita",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2023-11-07T21:57:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "3.5.0",
|
||||
@ -5238,6 +5435,77 @@
|
||||
],
|
||||
"time": "2023-11-08T05:53:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "rappasoft/laravel-authentication-log",
|
||||
"version": "v4.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rappasoft/laravel-authentication-log.git",
|
||||
"reference": "a916caaa979b1d18d679d8b063325fe547f691c6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/rappasoft/laravel-authentication-log/zipball/a916caaa979b1d18d679d8b063325fe547f691c6",
|
||||
"reference": "a916caaa979b1d18d679d8b063325fe547f691c6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/contracts": "^10.0|^11.0",
|
||||
"php": "^8.1",
|
||||
"spatie/laravel-package-tools": "^1.4.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"nunomaduro/collision": "^6.0",
|
||||
"orchestra/testbench": "^7.0",
|
||||
"pestphp/pest": "^1.21",
|
||||
"pestphp/pest-plugin-laravel": "^1.2",
|
||||
"spatie/laravel-ray": "^1.29",
|
||||
"vimeo/psalm": "^4.20"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Rappasoft\\LaravelAuthenticationLog\\LaravelAuthenticationLogServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Rappasoft\\LaravelAuthenticationLog\\": "src",
|
||||
"Rappasoft\\LaravelAuthenticationLog\\Database\\Factories\\": "database/factories"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Anthony Rappa",
|
||||
"email": "rappa819@gmail.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Log user authentication details and send new device notifications.",
|
||||
"homepage": "https://github.com/rappasoft/laravel-authentication-log",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"laravel-authentication-log",
|
||||
"rappasoft"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/rappasoft/laravel-authentication-log/issues",
|
||||
"source": "https://github.com/rappasoft/laravel-authentication-log/tree/v4.0.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/rappasoft",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-03-30T01:12:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "socialiteproviders/manager",
|
||||
"version": "v4.5.1",
|
||||
|
61
config/authentication-log.php
Normal file
61
config/authentication-log.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// The database table name
|
||||
// You can change this if the database keys get too long for your driver
|
||||
'table_name' => 'authentication_log',
|
||||
|
||||
// The database connection where the authentication_log table resides. Leave empty to use the default
|
||||
'db_connection' => null,
|
||||
|
||||
// The events the package listens for to log
|
||||
'events' => [
|
||||
'login' => \Illuminate\Auth\Events\Login::class,
|
||||
'failed' => \Illuminate\Auth\Events\Failed::class,
|
||||
'logout' => \Illuminate\Auth\Events\Logout::class,
|
||||
'logout-other-devices' => \Illuminate\Auth\Events\OtherDeviceLogout::class,
|
||||
],
|
||||
|
||||
'listeners' => [
|
||||
'login' => \Rappasoft\LaravelAuthenticationLog\Listeners\LoginListener::class,
|
||||
'failed' => \Rappasoft\LaravelAuthenticationLog\Listeners\FailedLoginListener::class,
|
||||
'logout' => \Rappasoft\LaravelAuthenticationLog\Listeners\LogoutListener::class,
|
||||
'logout-other-devices' => \Rappasoft\LaravelAuthenticationLog\Listeners\OtherDeviceLogoutListener::class,
|
||||
],
|
||||
|
||||
'notifications' => [
|
||||
'new-device' => [
|
||||
// Send the NewDevice notification
|
||||
'enabled' => env('NEW_DEVICE_NOTIFICATION', true),
|
||||
|
||||
// Use torann/geoip to attempt to get a location
|
||||
'location' => false,
|
||||
|
||||
// The Notification class to send
|
||||
'template' => \App\Notifications\SignedInWithNewDevice::class,
|
||||
],
|
||||
'failed-login' => [
|
||||
// Send the FailedLogin notification
|
||||
'enabled' => env('FAILED_LOGIN_NOTIFICATION', false),
|
||||
|
||||
// Use torann/geoip to attempt to get a location
|
||||
'location' => false,
|
||||
|
||||
// The Notification class to send
|
||||
'template' => \Rappasoft\LaravelAuthenticationLog\Notifications\FailedLogin::class,
|
||||
],
|
||||
],
|
||||
|
||||
// When the clean-up command is run, delete old logs greater than `purge` days
|
||||
// Don't schedule the clean-up command if you want to keep logs forever.
|
||||
'purge' => 365,
|
||||
|
||||
// If you are behind an CDN proxy, set 'behind_cdn.http_header_field' to the corresponding http header field of your cdn
|
||||
// For cloudflare you can have look at: https://developers.cloudflare.com/fundamentals/get-started/reference/http-request-headers/
|
||||
// 'behind_cdn' => [
|
||||
// 'http_header_field' => 'HTTP_CF_CONNECTING_IP' // used by Cloudflare
|
||||
// ],
|
||||
|
||||
// If you are not a cdn user, use false
|
||||
'behind_cdn' => false,
|
||||
];
|
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create(config('authentication-log.table_name'), function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('authenticatable');
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->timestamp('login_at')->nullable();
|
||||
$table->boolean('login_successful')->default(false);
|
||||
$table->timestamp('logout_at')->nullable();
|
||||
$table->boolean('cleared_by_user')->default(false);
|
||||
$table->json('location')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists(config('authentication-log.table_name'));
|
||||
}
|
||||
};
|
90
resources/js/components/AccessLogViewer.vue
Normal file
90
resources/js/components/AccessLogViewer.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<script setup>
|
||||
import SearchBox from '@/components/SearchBox.vue'
|
||||
import { useNotifyStore } from '@/stores/notify'
|
||||
import userService from '@/services/userService'
|
||||
import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
|
||||
const notify = useNotifyStore()
|
||||
|
||||
const props = defineProps({
|
||||
userId: [Number, String],
|
||||
lastOnly: Boolean,
|
||||
showSearch: Boolean
|
||||
})
|
||||
|
||||
const authentications = ref([])
|
||||
const isFetching = ref(false)
|
||||
const searched = ref('')
|
||||
|
||||
const visibleAuthentications = computed(() => {
|
||||
return authentications.value.filter(authentication => {
|
||||
return JSON.stringify(authentication)
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(searched.value);
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getAuthentications()
|
||||
})
|
||||
|
||||
/**
|
||||
* Gets user authentication logs
|
||||
*/
|
||||
function getAuthentications() {
|
||||
isFetching.value = true
|
||||
let limit = props.lastOnly ? 3 : false
|
||||
|
||||
userService.getauthentications(props.userId, limit, {returnError: true})
|
||||
.then(response => {
|
||||
authentications.value = response.data
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error(error)
|
||||
})
|
||||
.finally(() => {
|
||||
isFetching.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const deviceIcon = (device) => {
|
||||
switch (device) {
|
||||
case "phone":
|
||||
return 'mobile-screen'
|
||||
case "tablet":
|
||||
return 'tablet-screen-button'
|
||||
default:
|
||||
return 'display'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SearchBox v-if="props.showSearch" v-model:keyword="searched" :hasNoBackground="true" />
|
||||
<div v-if="visibleAuthentications.length > 0">
|
||||
<div v-for="authentication in visibleAuthentications" :key="authentication.id" class="list-item is-size-6 is-size-7-mobile has-text-grey is-flex is-justify-content-space-between">
|
||||
<div>
|
||||
<div >
|
||||
<span v-if="authentication.login_successful" v-html="$t('admin.successful_login_on', { login_at: authentication.login_at })" />
|
||||
<span v-else v-html="$t('admin.failed_login_on', { login_at: authentication.login_at })" />
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('commons.IP') }}: <span class="has-text-grey-light">{{ authentication.ip_address }}</span> - {{ $t('commons.browser') }}: <span class="has-text-grey-light">{{ authentication.browser }}</span> - {{ $t('commons.operating_system_short') }}: <span class="has-text-grey-light">{{ authentication.platform }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-align-self-center has-text-grey-darker">
|
||||
<font-awesome-layers class="fa-2x">
|
||||
<FontAwesomeIcon :icon="['fas', deviceIcon(authentication.device)]" transform="grow-6" fixed-width />
|
||||
<FontAwesomeIcon :icon="['fas', authentication.login_successful ? 'check' : 'times']" :transform="'shrink-7' + (authentication.device == 'desktop' ? ' up-2' : '')" fixed-width :class="authentication.login_successful ? 'has-text-success-dark' : 'has-text-danger-dark'" />
|
||||
</font-awesome-layers>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="authentications.length == 0" class="mt-4">
|
||||
{{ $t('commons.no_entry_yet') }}
|
||||
</div>
|
||||
<div v-else class="mt-5 pl-3">
|
||||
{{ $t('commons.no_result') }}
|
||||
</div>
|
||||
</template>
|
8
resources/js/icons.js
vendored
8
resources/js/icons.js
vendored
@ -48,6 +48,9 @@ import {
|
||||
faChevronRight,
|
||||
faSlash,
|
||||
faAlignLeft,
|
||||
faMobileScreen,
|
||||
faTabletScreenButton,
|
||||
faDisplay,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import {
|
||||
@ -110,7 +113,10 @@ library.add(
|
||||
faOpenid,
|
||||
faPaperPlane,
|
||||
faSlash,
|
||||
faAlignLeft
|
||||
faAlignLeft,
|
||||
faMobileScreen,
|
||||
faTabletScreenButton,
|
||||
faDisplay
|
||||
);
|
||||
|
||||
export default FontAwesomeIcon
|
1
resources/js/router/index.js
vendored
1
resources/js/router/index.js
vendored
@ -39,6 +39,7 @@ const router = createRouter({
|
||||
{ path: '/admin/users', name: 'admin.users', component: () => import('../views/admin/Users.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true } },
|
||||
{ path: '/admin/users/create', name: 'admin.createUser', component: () => import('../views/admin/users/Create.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true } },
|
||||
{ path: '/admin/users/:userId/manage', name: 'admin.manageUser', component: () => import('../views/admin/users/Manage.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true }, props: true },
|
||||
{ path: '/admin/logs/:userId/access', name: 'admin.logs.access', component: () => import('../views/admin/logs/Access.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true }, props: true },
|
||||
|
||||
{ path: '/login', name: 'login', component: () => import('../views/auth/Login.vue'), meta: { middlewares: [skipIfAuthProxy, setReturnTo], showAbout: true } },
|
||||
{ path: '/register', name: 'register', component: () => import('../views/auth/Register.vue'), meta: { middlewares: [skipIfAuthProxy, noRegistration, setReturnTo], showAbout: true } },
|
||||
|
9
resources/js/services/userService.js
vendored
9
resources/js/services/userService.js
vendored
@ -128,5 +128,14 @@ export default {
|
||||
revokeWebauthnCredentials(id, config = {}) {
|
||||
return apiClient.delete('/users/' + id + '/credentials', { ...config })
|
||||
},
|
||||
|
||||
/**
|
||||
* Get user's authentication logs
|
||||
*
|
||||
* @returns promise
|
||||
*/
|
||||
getauthentications(id, limit, config = {}) {
|
||||
return apiClient.get('/users/' + id + '/authentications' + (limit ? '?limit=' + limit : ''), { ...config })
|
||||
},
|
||||
|
||||
}
|
1
resources/js/stores/bus.js
vendored
1
resources/js/stores/bus.js
vendored
@ -9,6 +9,7 @@ export const useBusStore = defineStore({
|
||||
decodedUri: null,
|
||||
inManagementMode: false,
|
||||
editedGroupName: null,
|
||||
username: null,
|
||||
}
|
||||
},
|
||||
|
||||
|
46
resources/js/views/admin/logs/Access.vue
Normal file
46
resources/js/views/admin/logs/Access.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<script setup>
|
||||
import AccessLogViewer from '@/components/AccessLogViewer.vue'
|
||||
import userService from '@/services/userService'
|
||||
import { useNotifyStore } from '@/stores/notify'
|
||||
import { useBusStore } from '@/stores/bus'
|
||||
|
||||
const bus = useBusStore()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(async () => {
|
||||
getUser()
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
userId: [Number, String]
|
||||
})
|
||||
|
||||
const username = ref(bus.username ?? '')
|
||||
|
||||
/**
|
||||
* Gets the user from backend
|
||||
*/
|
||||
function getUser() {
|
||||
userService.getById(props.userId, {returnError: true})
|
||||
.then(response => {
|
||||
username.value = response.data.info.name
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ResponsiveWidthWrapper>
|
||||
<h1 class="title has-text-grey-dark">
|
||||
{{ $t('titles.admin.logs.access') }}
|
||||
</h1>
|
||||
<div class="block is-size-7-mobile">
|
||||
{{ $t('admin.access_log_legend_for_user', { username: username }) }} (#{{ props.userId }})
|
||||
</div>
|
||||
<AccessLogViewer :userId="props.userId" :lastOnly="false" :showSearch="true" />
|
||||
<!-- footer -->
|
||||
<VueFooter :showButtons="true">
|
||||
<ButtonBackCloseCancel :returnTo="{ name: 'admin.manageUser', params: { userId: props.userId }}" action="close" />
|
||||
</VueFooter>
|
||||
</ResponsiveWidthWrapper>
|
||||
</template>
|
@ -1,14 +1,16 @@
|
||||
<script setup>
|
||||
import CopyButton from '@/components/CopyButton.vue'
|
||||
import AccessLogViewer from '@/components/AccessLogViewer.vue'
|
||||
import userService from '@/services/userService'
|
||||
import { useNotifyStore } from '@/stores/notify'
|
||||
import { UseColorMode } from '@vueuse/components'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useBusStore } from '@/stores/bus'
|
||||
|
||||
const notify = useNotifyStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const user = useUserStore()
|
||||
const bus = useBusStore()
|
||||
|
||||
const isFetching = ref(false)
|
||||
const managedUser = ref(null)
|
||||
@ -31,6 +33,7 @@
|
||||
userService.getById(props.userId, {returnError: true})
|
||||
.then(response => {
|
||||
managedUser.value = response.data
|
||||
bus.username = managedUser.value.info.name
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error(error)
|
||||
@ -284,12 +287,19 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- logs -->
|
||||
<h2 class="title is-4 has-text-grey-light">{{ $t('admin.logs') }}</h2>
|
||||
<div class="block is-size-6 is-size-7-mobile has-text-grey">
|
||||
{{ $t('admin.registered_on_date', { date: managedUser.info.created_at }) }} - {{ $t('admin.last_seen_on_date', { date: managedUser.info.last_seen_at }) }}
|
||||
</div>
|
||||
<div class="block">
|
||||
<ul class="is-size-6 is-size-7-mobile">
|
||||
<li>{{ $t('admin.registered_on_date', { date: managedUser.info.created_at }) }}</li>
|
||||
<li>{{ $t('admin.last_seen_on_date', { date: managedUser.info.last_seen_at }) }}</li>
|
||||
</ul>
|
||||
<h3 class="title is-6 has-text-grey-light mb-0">{{ $t('admin.last_accesses') }}</h3>
|
||||
<AccessLogViewer :userId="props.userId" :lastOnly="true" />
|
||||
</div>
|
||||
<div class="block is-size-6 is-size-7-mobile has-text-grey">
|
||||
{{ $t('admin.access_log_has_more_entries') }} <router-link id="lnkFullLogs" :to="{ name: 'admin.logs.access', params: { userId: props.userId }}" >
|
||||
{{ $t('admin.see_full_log') }}
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- danger zone -->
|
||||
<h2 class="title is-4 has-text-danger">{{ $t('admin.danger_zone') }}</h2>
|
||||
|
@ -68,6 +68,13 @@
|
||||
'check_now' => 'Check now',
|
||||
'view_on_github' => 'View on Github',
|
||||
'x_is_available' => ':version is available',
|
||||
'successful_login_on' => 'Successful login on <span class="has-text-grey-light">:login_at</span>',
|
||||
'failed_login_on' => 'Failed login on <span class="has-text-grey-light">:login_at</span>',
|
||||
'last_accesses' => 'Last accesses',
|
||||
'see_full_log' => 'See full log',
|
||||
'browser_on_platform' => ':browser on :platform',
|
||||
'access_log_has_more_entries' => 'The access log is likely to contain more entries.',
|
||||
'access_log_legend_for_user' => 'Full access log for user :username',
|
||||
'forms' => [
|
||||
'use_encryption' => [
|
||||
'label' => 'Protect sensitive data',
|
||||
|
@ -81,4 +81,14 @@
|
||||
'information' => 'Information',
|
||||
'send' => 'Send',
|
||||
'optimize' => 'Optimize',
|
||||
'duration' => 'Duration',
|
||||
'from' => 'From',
|
||||
'using' => 'Using',
|
||||
'IP' => 'IP',
|
||||
'browser' => 'Browser',
|
||||
'operating_system_short' => 'OS',
|
||||
'no_entry_yet' => 'No entry yet',
|
||||
'time' => 'Time',
|
||||
'ip_address' => 'IP Address',
|
||||
'device' => 'Device',
|
||||
];
|
||||
|
@ -14,10 +14,17 @@
|
||||
*/
|
||||
|
||||
'hello' => 'Hello',
|
||||
'hello_user' => 'Hello :username,',
|
||||
'regards' => 'Regards',
|
||||
'test_email_settings' => [
|
||||
'subject' => '2FAuth test email',
|
||||
'reason' => 'You are receiving this email because you requested a test email to validate the email settings of your 2FAuth instance.',
|
||||
'success' => 'Good news, it works :)'
|
||||
],
|
||||
|
||||
'new_device' => [
|
||||
'subject' => 'New connection to 2FAuth',
|
||||
'resume' => 'A new device has just connected to your 2FAuth account.',
|
||||
'connection_details' => 'Here are the details of this connection',
|
||||
'recommandations' => 'If this was you, you can ignore this alert. If you suspect any suspicious activity on your account, please change your password.'
|
||||
],
|
||||
];
|
@ -51,5 +51,8 @@
|
||||
'users' => 'Users management',
|
||||
'createUser' => 'Create user',
|
||||
'manageUser' => 'Manage user',
|
||||
'logs' => [
|
||||
'access' => 'Access log'
|
||||
]
|
||||
]
|
||||
];
|
17
resources/views/emails/newDevice.blade.php
vendored
Normal file
17
resources/views/emails/newDevice.blade.php
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
@component('mail::message')
|
||||
@lang('notifications.hello_user', ['username' => $account->name])
|
||||
<br/><br/>
|
||||
**@lang('notifications.new_device.resume')**<br/>
|
||||
@lang('notifications.new_device.connection_details'):
|
||||
|
||||
<x-mail::panel>
|
||||
@lang('commons.time'): **{{ $time->toCookieString() }}**<br/>
|
||||
@lang('commons.ip_address'): **{{ $ipAddress }}**<br/>
|
||||
@lang('commons.device'): **@lang('admin.browser_on_platform', ['browser' => $browser, 'platform' => $platform])**<br/>
|
||||
</x-mail::panel>
|
||||
|
||||
@lang('notifications.new_device.recommandations')<br/>
|
||||
|
||||
@lang('notifications.regards'),<br/>
|
||||
{{ config('app.name') }}
|
||||
@endcomponent
|
@ -60,6 +60,7 @@
|
||||
* Routes protected by the api authentication guard and restricted to administrators
|
||||
*/
|
||||
Route::group(['middleware' => ['auth:api-guard', 'admin']], function () {
|
||||
Route::get('users/{user}/authentications', [UserManagerController::class, 'authentications'])->name('users.authentications');
|
||||
Route::patch('users/{user}/password/reset', [UserManagerController::class, 'resetPassword'])->name('users.password.reset');
|
||||
Route::patch('users/{user}/promote', [UserManagerController::class, 'promote'])->name('users.promote');
|
||||
Route::delete('users/{user}/pats', [UserManagerController::class, 'revokePATs'])->name('users.revoke.pats');
|
||||
|
@ -17,6 +17,10 @@
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Laravel\Passport\Http\Controllers\PersonalAccessTokenController;
|
||||
|
||||
// use App\Models\User;
|
||||
// use App\Notifications\SignedInWithNewDevice;
|
||||
// use Rappasoft\LaravelAuthenticationLog\Models\AuthenticationLog;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Web Routes
|
||||
@ -89,6 +93,12 @@
|
||||
return csrf_token();
|
||||
});
|
||||
|
||||
// Route::get('/notification', function () {
|
||||
// $user = User::find(1);
|
||||
// return (new SignedInWithNewDevice(AuthenticationLog::find(9)))
|
||||
// ->toMail($user);
|
||||
// });
|
||||
|
||||
/**
|
||||
* Route for the main landing view
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user