feat: implement gate authentication with login and logout functionality
This commit is contained in:
@@ -4,6 +4,9 @@ APP_KEY=
|
|||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
# ── App Gate Password ─────────────────────────────────────────────────────────
|
||||||
|
APP_PASSWORD=
|
||||||
|
|
||||||
APP_LOCALE=en
|
APP_LOCALE=en
|
||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
APP_FAKER_LOCALE=en_US
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|||||||
44
app/Http/Controllers/GateController.php
Normal file
44
app/Http/Controllers/GateController.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Http/Middleware/GateAuth.php
Normal file
26
app/Http/Middleware/GateAuth.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Middleware\GateAuth;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
@@ -11,7 +12,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
//
|
$middleware->web(append: [
|
||||||
|
GateAuth::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ return [
|
|||||||
|
|
||||||
'name' => env('APP_NAME', 'Laravel'),
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
|
'password' => env('APP_PASSWORD'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application Environment
|
| Application Environment
|
||||||
|
|||||||
111
resources/views/gate/login.blade.php
Normal file
111
resources/views/gate/login.blade.php
Normal 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>
|
||||||
@@ -134,6 +134,15 @@
|
|||||||
class="nav-item {{ request()->routeIs('settings.*') ? 'active' : '' }}">
|
class="nav-item {{ request()->routeIs('settings.*') ? 'active' : '' }}">
|
||||||
<span class="text-lg shrink-0">🔔</span> Notifications
|
<span class="text-lg shrink-0">🔔</span> Notifications
|
||||||
</a>
|
</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>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Http\Controllers\BrandContractController;
|
use App\Http\Controllers\BrandContractController;
|
||||||
|
use App\Http\Controllers\GateController;
|
||||||
use App\Http\Controllers\NoteController;
|
use App\Http\Controllers\NoteController;
|
||||||
use App\Http\Controllers\ClientCategoryController;
|
use App\Http\Controllers\ClientCategoryController;
|
||||||
use App\Http\Controllers\ClientController;
|
use App\Http\Controllers\ClientController;
|
||||||
@@ -18,6 +19,11 @@ use App\Http\Controllers\SearchController;
|
|||||||
use App\Http\Controllers\SocialHubController;
|
use App\Http\Controllers\SocialHubController;
|
||||||
use Illuminate\Support\Facades\Route;
|
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');
|
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
|
||||||
|
|
||||||
// CRM – Clients
|
// CRM – Clients
|
||||||
|
|||||||
Reference in New Issue
Block a user