feat: implement gate authentication with login and logout functionality

This commit is contained in:
I-Fun
2026-04-03 11:12:10 +07:00
parent 3f18dbaadf
commit a0b294fa1c
8 changed files with 205 additions and 1 deletions

View File

@@ -4,6 +4,9 @@ APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
# ── App Gate Password ─────────────────────────────────────────────────────────
APP_PASSWORD=
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class GateController extends Controller
{
public function login(): View
{
return view('gate.login');
}
public function attempt(Request $request): RedirectResponse
{
$request->validate([
'password' => 'required|string',
]);
$expected = config('app.password');
if (! $expected || $request->input('password') !== $expected) {
return back()->withErrors(['password' => 'Wrong password. Please try again.']);
}
// Store flag in session; expires with session lifetime (24 h in .env)
$request->session()->put('gate_authenticated', true);
$request->session()->regenerate();
return redirect()->intended(route('dashboard'));
}
public function logout(Request $request): RedirectResponse
{
$request->session()->forget('gate_authenticated');
$request->session()->regenerate();
return redirect()->route('gate.login');
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class GateAuth
{
public function handle(Request $request, Closure $next): Response
{
// Allow the login routes through without auth check
if ($request->routeIs('gate.login', 'gate.attempt', 'gate.logout')) {
return $next($request);
}
if (! $request->session()->get('gate_authenticated')) {
return redirect()->route('gate.login');
}
return $next($request);
}
}

View File

@@ -1,5 +1,6 @@
<?php
use App\Http\Middleware\GateAuth;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
@@ -11,7 +12,9 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
$middleware->web(append: [
GateAuth::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//

View File

@@ -15,6 +15,8 @@ return [
'name' => env('APP_NAME', 'Laravel'),
'password' => env('APP_PASSWORD'),
/*
|--------------------------------------------------------------------------
| Application Environment

View File

@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#FFB7C5">
<title>KOL Manager — Login</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-cream font-sans antialiased min-h-screen flex items-center justify-center px-4"
x-data="{ show: false, shaking: false }">
{{-- ── Decorative blobs ───────────────────────────────────────── --}}
<div class="fixed inset-0 overflow-hidden pointer-events-none select-none" aria-hidden="true">
<div class="absolute -top-32 -left-32 w-96 h-96 rounded-full opacity-20"
style="background: radial-gradient(circle, #FFB7C5, transparent)"></div>
<div class="absolute -bottom-32 -right-32 w-96 h-96 rounded-full opacity-20"
style="background: radial-gradient(circle, #E8D5F5, transparent)"></div>
<div class="absolute top-1/2 left-1/4 w-48 h-48 rounded-full opacity-10"
style="background: radial-gradient(circle, #FFC8DD, transparent)"></div>
</div>
{{-- ── Login card ─────────────────────────────────────────────── --}}
<div class="relative z-10 w-full max-w-sm">
{{-- Logo / header --}}
<div class="text-center mb-8 select-none">
<div class="text-7xl mb-3 inline-block animate-bounce" style="animation-duration:2s">🐱</div>
<h1 class="text-2xl font-extrabold text-pink-500 leading-tight">KOL Manager</h1>
<p class="text-sm text-gray-400 mt-1">Your Purr-fect Assistant</p>
</div>
{{-- Card --}}
<div class="bg-white rounded-3xl shadow-xl border border-pink-100 p-8">
<h2 class="text-base font-bold text-gray-700 mb-1">Welcome back 👋</h2>
<p class="text-xs text-gray-400 mb-6">Enter the app password to continue.</p>
{{-- Error --}}
@error('password')
<div class="flex items-center gap-2 bg-red-50 border border-red-200 text-red-600 text-xs px-4 py-3 rounded-2xl mb-4"
x-data x-init="$el.closest('[x-data]').shaking = true; setTimeout(() => $el.closest('[x-data]').shaking = false, 500)">
<span class="text-base">😿</span>
<span>{{ $message }}</span>
</div>
@enderror
<form method="POST" action="{{ route('gate.attempt') }}"
:class="shaking ? 'animate-[shake_0.4s_ease-in-out]' : ''">
@csrf
{{-- Password field --}}
<div class="relative mb-5">
<label for="password" class="block text-xs font-semibold text-gray-500 mb-1.5">
Password
</label>
<div class="relative">
<input
:type="show ? 'text' : 'password'"
id="password"
name="password"
autocomplete="current-password"
autofocus
placeholder="Enter password…"
class="w-full rounded-2xl border border-pink-100 bg-pink-50/40 px-4 py-3 pr-11 text-sm text-gray-700
placeholder-gray-300 outline-none transition
focus:border-pink-300 focus:ring-2 focus:ring-pink-100
@error('password') border-red-300 bg-red-50/40 @enderror"
>
{{-- Show/hide toggle --}}
<button type="button"
@click="show = !show"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-pink-400 transition-colors select-none"
tabindex="-1"
:title="show ? 'Hide password' : 'Show password'">
<span x-text="show ? '🙈' : '👁️'" class="text-base leading-none"></span>
</button>
</div>
</div>
{{-- Submit --}}
<button type="submit"
class="w-full py-3 rounded-2xl font-bold text-sm text-white shadow-md
transition-all duration-200 active:scale-95 hover:shadow-lg hover:brightness-105"
style="background: linear-gradient(135deg, #FFB7C5, #E8A0B4)">
🐾 Enter the App
</button>
</form>
</div>
{{-- Footer hint --}}
<p class="text-center text-xs text-gray-400 mt-5">
<!-- Session lasts 24 hours after login. -->
</p>
</div>
{{-- Shake keyframe --}}
<style>
@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-6px); }
40% { transform: translateX(6px); }
60% { transform: translateX(-4px); }
80% { transform: translateX(4px); }
}
</style>
</body>
</html>

View File

@@ -134,6 +134,15 @@
class="nav-item {{ request()->routeIs('settings.*') ? 'active' : '' }}">
<span class="text-lg shrink-0">🔔</span> Notifications
</a>
{{-- Logout --}}
<form method="POST" action="{{ route('gate.logout') }}" class="mt-auto pt-4">
@csrf
<button type="submit"
class="nav-item w-full text-left text-gray-400 hover:text-red-400 hover:bg-red-50 transition-colors">
<span class="text-lg shrink-0">🚪</span> Lock App
</button>
</form>
</nav>
</aside>

View File

@@ -3,6 +3,7 @@
declare(strict_types=1);
use App\Http\Controllers\BrandContractController;
use App\Http\Controllers\GateController;
use App\Http\Controllers\NoteController;
use App\Http\Controllers\ClientCategoryController;
use App\Http\Controllers\ClientController;
@@ -18,6 +19,11 @@ use App\Http\Controllers\SearchController;
use App\Http\Controllers\SocialHubController;
use Illuminate\Support\Facades\Route;
// ── Gate Authentication ───────────────────────────────────────────────────────
Route::get('/login', [GateController::class, 'login'])->name('gate.login');
Route::post('/login', [GateController::class, 'attempt'])->name('gate.attempt');
Route::post('/logout', [GateController::class, 'logout'])->name('gate.logout');
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
// CRM – Clients