[FIX] 解決手機重複登入日誌問題並新增裝置詳細資訊偵測
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 59s

This commit is contained in:
2026-03-13 10:21:43 +08:00
parent 6fab048461
commit bb5d212569
7 changed files with 277 additions and 11 deletions

View File

@@ -34,10 +34,36 @@ class LogSuccessfulLogin
*/
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([
'user_id' => $event->user->id,
'ip_address' => $this->request->ip(),
'user_agent' => $this->request->userAgent(),
'ip_address' => $ip,
'user_agent' => $userAgent,
'device_type' => $deviceType,
'browser' => $agent->browser(),
'platform' => $agent->platform(),
'login_at' => now(),
]);
}

View File

@@ -13,6 +13,9 @@ class UserLoginLog extends Model
'user_id',
'ip_address',
'user_agent',
'device_type',
'browser',
'platform',
'login_at',
];

View File

@@ -25,9 +25,6 @@ class AppServiceProvider extends ServiceProvider
if (!$this->app->isLocal()) {
\Illuminate\Support\Facades\URL::forceScheme('https');
}
// 記錄使用者成功登入的歷史
Event::listen(Login::class, LogSuccessfulLogin::class);
}
}

View File

@@ -10,6 +10,7 @@
"require": {
"php": "^8.2",
"guzzlehttp/guzzle": "^7.8",
"jenssegers/agent": "^2.6",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.3",
"laravel/tinker": "^2.10.1"

199
composer.lock generated
View File

@@ -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": "dc689fd91200cf19e401759d009094a3",
"content-hash": "0c5398ab8233c21548345608b86027cc",
"packages": [
{
"name": "brick/math",
@@ -1052,6 +1052,141 @@
],
"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",
"version": "v12.53.0",
@@ -2082,6 +2217,68 @@
],
"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",
"version": "3.10.0",

View File

@@ -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']);
});
}
};

View File

@@ -33,10 +33,22 @@
{{ __('Success') }}
</span>
</div>
<p class="text-xs font-medium text-slate-400 dark:text-slate-500 break-all leading-relaxed" title="{{ $log->user_agent }}">
<span class="font-bold text-slate-500 dark:text-slate-400 mr-1 italic">{{ __('Device:') }}</span>
{{ $log->user_agent }}
</p>
<div class="flex items-center gap-x-2 text-xs font-medium text-slate-400 dark:text-slate-500">
<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">
@if($log->device_type === 'mobile')
<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 class="shrink-0 text-right">
<p class="text-[10px] font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-1">