[FIX] 解決手機重複登入日誌問題並新增裝置詳細資訊偵測
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 59s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 59s
This commit is contained in:
@@ -34,10 +34,36 @@ class LogSuccessfulLogin
|
|||||||
*/
|
*/
|
||||||
public function handle(Login $event)
|
public function handle(Login $event)
|
||||||
{
|
{
|
||||||
|
$ip = $this->request->ip();
|
||||||
|
$userAgent = $this->request->userAgent();
|
||||||
|
|
||||||
|
// 防重覆機制 (Debouncing): 10 秒內同使用者、同 IP 的記錄視為重複
|
||||||
|
$recentLog = UserLoginLog::where('user_id', $event->user->id)
|
||||||
|
->where('ip_address', $ip)
|
||||||
|
->where('login_at', '>=', now()->subSeconds(10))
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($recentLog) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$agent = new \Jenssegers\Agent\Agent();
|
||||||
|
$agent->setUserAgent($userAgent);
|
||||||
|
|
||||||
|
$deviceType = 'desktop';
|
||||||
|
if ($agent->isTablet()) {
|
||||||
|
$deviceType = 'tablet';
|
||||||
|
} elseif ($agent->isMobile()) {
|
||||||
|
$deviceType = 'mobile';
|
||||||
|
}
|
||||||
|
|
||||||
UserLoginLog::create([
|
UserLoginLog::create([
|
||||||
'user_id' => $event->user->id,
|
'user_id' => $event->user->id,
|
||||||
'ip_address' => $this->request->ip(),
|
'ip_address' => $ip,
|
||||||
'user_agent' => $this->request->userAgent(),
|
'user_agent' => $userAgent,
|
||||||
|
'device_type' => $deviceType,
|
||||||
|
'browser' => $agent->browser(),
|
||||||
|
'platform' => $agent->platform(),
|
||||||
'login_at' => now(),
|
'login_at' => now(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ class UserLoginLog extends Model
|
|||||||
'user_id',
|
'user_id',
|
||||||
'ip_address',
|
'ip_address',
|
||||||
'user_agent',
|
'user_agent',
|
||||||
|
'device_type',
|
||||||
|
'browser',
|
||||||
|
'platform',
|
||||||
'login_at',
|
'login_at',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
if (!$this->app->isLocal()) {
|
if (!$this->app->isLocal()) {
|
||||||
\Illuminate\Support\Facades\URL::forceScheme('https');
|
\Illuminate\Support\Facades\URL::forceScheme('https');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 記錄使用者成功登入的歷史
|
|
||||||
Event::listen(Login::class, LogSuccessfulLogin::class);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"guzzlehttp/guzzle": "^7.8",
|
"guzzlehttp/guzzle": "^7.8",
|
||||||
|
"jenssegers/agent": "^2.6",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/sanctum": "^4.3",
|
"laravel/sanctum": "^4.3",
|
||||||
"laravel/tinker": "^2.10.1"
|
"laravel/tinker": "^2.10.1"
|
||||||
@@ -67,4 +68,4 @@
|
|||||||
},
|
},
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"prefer-stable": true
|
"prefer-stable": true
|
||||||
}
|
}
|
||||||
|
|||||||
199
composer.lock
generated
199
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "dc689fd91200cf19e401759d009094a3",
|
"content-hash": "0c5398ab8233c21548345608b86027cc",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
@@ -1052,6 +1052,141 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-08-22T14:27:06+00:00"
|
"time": "2025-08-22T14:27:06+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "jaybizzle/crawler-detect",
|
||||||
|
"version": "v1.3.7",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/JayBizzle/Crawler-Detect.git",
|
||||||
|
"reference": "7f7a45b5d5df9c95ba6b2008544e6cf8e66de6f5"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/7f7a45b5d5df9c95ba6b2008544e6cf8e66de6f5",
|
||||||
|
"reference": "7f7a45b5d5df9c95ba6b2008544e6cf8e66de6f5",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=7.1.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^4.8|^5.5|^6.5|^7.5|^8.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.3.7"
|
||||||
|
},
|
||||||
|
"time": "2026-02-02T19:15:54+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": {
|
||||||
|
"laravel": {
|
||||||
|
"aliases": {
|
||||||
|
"Agent": "Jenssegers\\Agent\\Facades\\Agent"
|
||||||
|
},
|
||||||
|
"providers": [
|
||||||
|
"Jenssegers\\Agent\\AgentServiceProvider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.0-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": "laravel/framework",
|
"name": "laravel/framework",
|
||||||
"version": "v12.53.0",
|
"version": "v12.53.0",
|
||||||
@@ -2082,6 +2217,68 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-01-15T06:54:53+00:00"
|
"time": "2026-01-15T06:54:53+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",
|
"name": "monolog/monolog",
|
||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('user_login_logs', function (Blueprint $table) {
|
||||||
|
$table->string('device_type')->nullable()->after('user_agent'); // desktop, mobile, tablet
|
||||||
|
$table->string('browser')->nullable()->after('device_type');
|
||||||
|
$table->string('platform')->nullable()->after('browser');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('user_login_logs', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['device_type', 'browser', 'platform']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -33,10 +33,22 @@
|
|||||||
{{ __('Success') }}
|
{{ __('Success') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs font-medium text-slate-400 dark:text-slate-500 break-all leading-relaxed" title="{{ $log->user_agent }}">
|
<div class="flex items-center gap-x-2 text-xs font-medium text-slate-400 dark:text-slate-500">
|
||||||
<span class="font-bold text-slate-500 dark:text-slate-400 mr-1 italic">{{ __('Device:') }}</span>
|
<div class="flex items-center gap-x-1.5 px-2 py-1 rounded-lg bg-slate-100 dark:bg-slate-800/50 border border-slate-200/50 dark:border-slate-700/50">
|
||||||
{{ $log->user_agent }}
|
@if($log->device_type === 'mobile')
|
||||||
</p>
|
<svg class="size-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"/></svg>
|
||||||
|
@elseif($log->device_type === 'tablet')
|
||||||
|
<svg class="size-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"/></svg>
|
||||||
|
@else
|
||||||
|
<svg class="size-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
||||||
|
@endif
|
||||||
|
<span class="font-bold text-slate-500 dark:text-slate-400">{{ $log->platform ?: 'Unknown OS' }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="size-1 rounded-full bg-slate-300 dark:bg-slate-600"></span>
|
||||||
|
<span class="text-slate-500 dark:text-slate-400">
|
||||||
|
{{ $log->browser ?: 'Unknown Browser' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="shrink-0 text-right">
|
<div class="shrink-0 text-right">
|
||||||
<p class="text-[10px] font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-1">
|
<p class="text-[10px] font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user