feat(app): Build complete UI for Siswa, Guru, and Admin roles with refactored auth

This commit is contained in:
zhadaarsita 2025-10-20 12:14:43 +07:00
parent 98679fec62
commit 187bb9d9af
15 changed files with 466 additions and 256 deletions

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class AdminLoginController extends Controller
{
// Menampilkan form login admin
public function create(): View
{
return view('auth.admin-login');
}
// Memproses login admin
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate(); // Menjalankan logika ketat di LoginRequest
$request->session()->regenerate();
return redirect()->route('admin.dashboard');
}
}

View File

@ -14,9 +14,14 @@ class AuthenticatedSessionController extends Controller
/** /**
* Display the login view. * Display the login view.
*/ */
public function create(): View public function create(Request $request): View
{ {
return view('auth.login'); // Ambil 'role' dari URL, jika tidak ada, defaultnya 'siswa'
$role = $request->query('role', 'siswa');
return view('auth.login', [
'role' => $role
]);
} }
/** /**
@ -24,22 +29,12 @@ public function create(): View
*/ */
public function store(LoginRequest $request): RedirectResponse public function store(LoginRequest $request): RedirectResponse
{ {
$request->authenticate(); $request->authenticate(); // Menjalankan logika di LoginRequest
$request->session()->regenerate(); $request->session()->regenerate();
// Ambil data user dari session // Karena login sudah dijamin benar, cukup arahkan ke dashboard umum
$userData = session('user_data'); return redirect()->intended(route('dashboard'));
// Cek role dan redirect sesuai role
if ($userData && isset($userData['role']) && $userData['role'] === 'penjaga perpus') {
return redirect()->route('admin.dashboard');
}
// Default redirect ke dashboard siswa
return redirect()->route('dashboard');
} }
/** /**
* Destroy an authenticated session. * Destroy an authenticated session.
*/ */
@ -47,11 +42,11 @@ public function destroy(Request $request): RedirectResponse
{ {
Auth::guard('web')->logout(); Auth::guard('web')->logout();
$request->session()->forget('user_data'); $request->session()->forget('user_data');
$request->session()->invalidate(); $request->session()->invalidate();
$request->session()->regenerateToken(); $request->session()->regenerateToken();
return redirect('/'); return redirect('/');
} }
} }

View File

@ -1,25 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class AuthenticateFromSessionData
{
public function handle(Request $request, Closure $next): Response
{
if (session()->has('user_data') && !Auth::check()) {
$userArray = session('user_data');
$userArray['name'] = $userArray['nama_lengkap'];
$userModel = new User();
$userModel->forceFill($userArray);
Auth::login($userModel);
}
return $next($request);
}
}

View File

@ -4,25 +4,16 @@
use Closure; use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
class CheckRole class CheckRole
{ {
/** public function handle(Request $request, Closure $next, ...$roles): Response
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next, string $role): Response
{ {
$userData = session('user_data'); if (!Auth::check() || !in_array(Auth::user()->role, $roles)) {
return redirect()->route('login')
// Cek apakah user sudah login ->with('error', 'Akses ditolak. Anda tidak memiliki izin untuk mengakses halaman tersebut.');
if (!$userData) {
return redirect()->route('login');
}
// Cek apakah role sesuai
if (!isset($userData['role']) || $userData['role'] !== $role) {
abort(403, 'Akses ditolak. Anda tidak memiliki izin untuk mengakses halaman ini.');
} }
return $next($request); return $next($request);

View File

@ -1,23 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SessionAuthMiddleware
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
// Cek apakah user_data ada di session
if (!session()->has('user_data')) {
return redirect()->route('login');
}
return $next($request);
}
}

View File

@ -2,71 +2,133 @@
namespace App\Http\Requests\Auth; namespace App\Http\Requests\Auth;
use App\Models\User;
use App\Services\DummyDataService; use App\Services\DummyDataService;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Events\Lockout;
class LoginRequest extends FormRequest class LoginRequest extends FormRequest
{ {
/** /**
* Determine if the user is authorized to make this request. * Menentukan apakah pengguna diizinkan untuk membuat request ini.
* Selalu true karena semua orang boleh mencoba login.
*/ */
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
} }
/**
* Mendapatkan aturan validasi yang berlaku untuk request ini.
* Aturan ini dinamis, berubah tergantung pada 'role' yang dikirim dari form.
*/
public function rules(): array public function rules(): array
{ {
// Jika form mengirim 'role' dengan nilai 'siswa'...
if ($this->input('role') === 'siswa') {
// ...maka validasi input 'nisn' dan 'password'.
return [
'nisn' => ['required', 'string'],
'password' => ['required', 'string'],
];
}
// Jika tidak (untuk 'guru' dan 'penjaga perpus'), validasi 'email' dan 'password'.
return [ return [
'nisn' => ['required', 'string'], 'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'], 'password' => ['required', 'string'],
]; ];
} }
/**
* Mencoba untuk mengautentikasi kredensial dari request.
* Ini adalah "otak" dari proses login yang berisi logika paling penting.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void public function authenticate(): void
{ {
// Langkah 1: Pastikan pengguna tidak mencoba login terlalu sering (mencegah brute-force).
$this->ensureIsNotRateLimited(); $this->ensureIsNotRateLimited();
$allSiswa = DummyDataService::getAllSiswa(); // Ambil data yang dikirim dari form login.
$inputNisn = $this->input('nisn'); $roleDariForm = $this->input('role');
$allUsers = DummyDataService::getAllSiswa();
$inputPassword = $this->input('password'); $inputPassword = $this->input('password');
$userArray = null;
// Tentukan field mana yang akan menerima pesan error jika gagal (nisn atau email).
$errorField = $this->filled('nisn') ? 'nisn' : 'email';
$userArray = collect($allSiswa)->firstWhere('nisn', $inputNisn); // Langkah 2: Cari data pengguna berdasarkan input yang diberikan.
if ($this->filled('nisn')) {
if ($userArray && $userArray['password'] === $inputPassword) { // Jika form diisi dengan 'nisn', cari pengguna berdasarkan 'nisn'.
// Simpan ke session $userArray = collect($allUsers)->firstWhere('nisn', $this->input('nisn'));
session(['user_data' => $userArray]); } else {
// Jika tidak, cari pengguna berdasarkan 'email'.
// Set redirect intention berdasarkan role $userArray = collect($allUsers)->firstWhere('email', $this->input('email'));
if (isset($userArray['role']) && $userArray['role'] === 'penjaga perpus') {
session()->put('url.intended', route('admin.dashboard'));
}
RateLimiter::clear($this->throttleKey());
return;
} }
// Langkah 3: Lakukan Pengecekan Kredensial dan Role.
// Cek #1: Apakah pengguna ditemukan DAN password yang dimasukkan cocok?
if ($userArray && $userArray['password'] === $inputPassword) {
// Cek #2: Jika kredensial benar, apakah role pengguna sesuai dengan form yang digunakan?
if (isset($userArray['role']) && $userArray['role'] === $roleDariForm) {
// --- SEMUA SYARAT TERPENUHI ---
// Buat objek User dari data dummy.
$userModel = new User();
$userArray['name'] = $userArray['nama_lengkap'];
$userModel->forceFill($userArray);
// Loginkan pengguna secara resmi ke dalam sistem.
Auth::login($userModel);
// Reset hitungan percobaan login yang gagal.
RateLimiter::clear($this->throttleKey());
return; // Proses autentikasi berhasil.
} else {
// --- KASUS GAGAL: KREDENSIAL BENAR, TAPI ROLE SALAH ---
// Tambah hitungan percobaan login yang gagal.
RateLimiter::hit($this->throttleKey());
// Ambil nama role asli pengguna untuk ditampilkan di pesan error.
$actualRole = Str::title($userArray['role'] ?? 'Tidak Dikenal');
// Lemparkan error validasi khusus 'forbidden' dengan pesan yang jelas.
throw ValidationException::withMessages([
'forbidden' => "Akses ditolak. Akun ini terdaftar sebagai {$actualRole}.",
]);
}
}
// --- KASUS GAGAL: KREDENSIAL TIDAK COCOK ---
// Jika pengguna tidak ditemukan atau password salah.
RateLimiter::hit($this->throttleKey()); RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'nisn' => trans('auth.failed'), $errorField => trans('auth.failed'), // Pesan error umum "These credentials do not match...".
]); ]);
} }
/**
* Memastikan request login tidak dibatasi karena terlalu banyak percobaan.
*/
public function ensureIsNotRateLimited(): void public function ensureIsNotRateLimited(): void
{ {
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { // Jika percobaan belum melebihi 5 kali, lanjutkan.
if (!RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return; return;
} }
// Jika sudah lebih dari 5 kali, lemparkan error 'throttle'.
event(new Lockout($this)); event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey()); $seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'email' => trans('auth.throttle', [ 'email' => trans('auth.throttle', [
'seconds' => $seconds, 'seconds' => $seconds,
@ -76,10 +138,13 @@ public function ensureIsNotRateLimited(): void
} }
/** /**
* Get the rate limiting throttle key for the request. * Mendapatkan kunci throttle untuk request ini.
* Kunci ini unik untuk setiap pengguna (berdasarkan nisn/email) dan alamat IP.
*/ */
public function throttleKey(): string public function throttleKey(): string
{ {
return Str::transliterate(Str::lower($this->string('nisn')).'|'.$this->ip()); // Gunakan 'nisn' jika ada, jika tidak, gunakan 'email' sebagai identitas.
$loginIdentifier = $this->input('nisn') ?: $this->input('email');
return Str::transliterate(Str::lower($loginIdentifier) . '|' . $this->ip());
} }
} }

View File

@ -23,10 +23,8 @@ public static function getAllSiswa(): array
], ],
[ [
'id' => 2, 'id' => 2,
'nisn' => '1122334455',
'nama_lengkap' => 'Budi Santoso', 'nama_lengkap' => 'Budi Santoso',
'email' => 'budi.santoso@smkn1perpus.sch.id', 'email' => 'budi.santoso@smkn1perpus.sch.id',
'nomor_hp' => '081122334455',
'password' => 'password', 'password' => 'password',
'role' => 'penjaga perpus', 'role' => 'penjaga perpus',
], ],
@ -54,10 +52,8 @@ public static function getAllSiswa(): array
], ],
[ [
'id' => 5, 'id' => 5,
'nisn' => '2233445566',
'nama_lengkap' => 'Rina Marlina', 'nama_lengkap' => 'Rina Marlina',
'email' => 'rina.marlina@smkn1perpus.sch.id', 'email' => 'rina.marlina@smkn1perpus.sch.id',
'nomor_hp' => '081223344556',
'password' => 'password', 'password' => 'password',
'role' => 'guru', 'role' => 'guru',
], ],

View File

@ -11,14 +11,10 @@
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [ $middleware->alias([
\App\Http\Middleware\AuthenticateFromSessionData::class, 'role' => \App\Http\Middleware\CheckRole::class,
]); ]);
$middleware->alias([ })
'session.auth' => \App\Http\Middleware\SessionAuthMiddleware::class,
'role' => \App\Http\Middleware\CheckRole::class,
]);
})
->withProviders([ ->withProviders([
App\Providers\AuthServiceProvider::class, App\Providers\AuthServiceProvider::class,
]) ])

View File

@ -317,3 +317,29 @@ $transition: all 0.3s ease;
.book-option[style*="display: none"] { .book-option[style*="display: none"] {
display: none !important; display: none !important;
} }
// ===================================
// WELCOME PAGE & Login Page Styles
// ===================================
.hero-gradient {
background: linear-gradient(135deg, map-get($theme-colors, "primary") 0%, color.adjust(map-get($theme-colors, "primary"), $lightness: 10%) 100%);
}
.role-card {
display: block;
.card {
transition: $transition;
border-radius: 20px;
&:hover {
transform: translateY(-4px);
box-shadow: $shadow-md;
}
}
}
.info-panel {
background: linear-gradient(135deg, map-get($theme-colors, "primary") 0%, color.adjust(map-get($theme-colors, "primary"), $lightness: 10%) 100%);
}

View File

@ -0,0 +1,40 @@
<x-guest-layout>
@if ($errors->has('forbidden'))
<div class="alert alert-danger d-flex align-items-center" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<div>{{ $errors->first('forbidden') }}</div>
</div>
@endif
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('admin.login.store') }}">
@csrf
<input type="hidden" name="role" value="penjaga perpus">
<div class="text-center mb-4">
<i class="bi bi-shield-lock-fill text-primary fs-1 mb-3"></i>
<h3 class="fw-bold text-primary">Login Petugas</h3>
<p class="text-muted">Halaman ini khusus untuk Penjaga Perpustakaan.</p>
</div>
<div class="mb-3">
<label for="email" class="form-label">Alamat Email</label>
<input id="email" class="form-control" type="email" name="email" required autofocus />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div class="mb-3">
<label for="password" class="form-label">Kata Sandi</label>
<input id="password" class="form-control" type="password" name="password" required />
</div>
<div class="d-grid mt-4">
<button type="submit" class="btn btn-primary btn-lg">Masuk</button>
</div>
<p class="mt-4 text-center text-muted small">
Kembali ke <a href="/" class="fw-semibold text-decoration-none">halaman utama</a>.
</p>
</form>
</x-guest-layout>

View File

@ -1,75 +1,53 @@
<x-guest-layout> <x-guest-layout>
<x-auth-session-status class="mb-4" :status="session('status')" /> @if ($errors->has('forbidden'))
<div class="alert alert-danger d-flex align-items-center" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<div>{{ $errors->first('forbidden') }}</div>
</div>
@endif
<form method="POST" action="{{ route('login') }}"> <form method="POST" action="{{ route('login') }}">
@csrf @csrf
<input type="hidden" name="role" value="{{ $role }}">
<div class="text-center mb-4"> <div class="text-center mb-4">
<h3 class="fw-bold text-primary">Login Siswa</h3> {{-- Judul dinamis --}}
<p class="text-muted">Masukan NISN dan kata sandi Anda.</p> <h3 class="fw-bold text-primary">Login {{ Str::title($role) }}</h3>
<p class="text-muted">
@if ($role == 'siswa')
Masukan NISN dan kata sandi Anda.
@else
Masukan Email dan kata sandi Anda.
@endif
</p>
</div> </div>
<div class="mb-3"> {{-- Form dinamis --}}
<label for="nisn" class="form-label">Nomor Induk Siswa Nasional (NISN)</label> @if($role == 'siswa')
<input id="nisn" class="form-control bg-body-tertiary @error('nisn') is-invalid @enderror" <div class="mb-3">
type="text" name="nisn" value="{{ old('nisn') }}" required autofocus autocomplete="username" /> <label for="nisn" class="form-label">Nomor Induk Siswa Nasional (NISN)</label>
@error('nisn') <input id="nisn" class="form-control" type="text" name="nisn" required autofocus />
<div class="invalid-feedback">{{ $message }}</div> <x-input-error :messages="$errors->get('nisn')" class="mt-2" /> {{-- <-- Pastikan ini ada --}}
@enderror </div>
</div> @else
<div class="mb-3">
<label for="email" class="form-label">Alamat Email</label>
<input id="email" class="form-control" type="email" name="email" required autofocus />
<x-input-error :messages="$errors->get('email')" class="mt-2" /> {{-- <-- Pastikan ini ada --}}
</div>
@endif
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">Kata Sandi</label> <label for="password" class="form-label">Kata Sandi</label>
<div class="input-group"> <input id="password" class="form-control" type="password" name="password" required />
<input id="password" class="form-control bg-body-tertiary @error('password') is-invalid @enderror"
type="password" name="password" required autocomplete="current-password" />
<span class="input-group-text bg-body-tertiary" id="togglePassword" style="cursor: pointer;">
<i class="bi bi-eye-slash-fill"></i>
</span>
@error('password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div> </div>
<div class="d-flex justify-content-end align-items-center mb-3"> <div class="d-grid mt-4">
@if (Route::has('password.request')) <button type="submit" class="btn btn-primary btn-lg">Masuk</button>
<a class="text-decoration-none small" href="{{ route('password.request') }}">
Lupa password?
</a>
@endif
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">
Masuk
</button>
</div> </div>
<p class="mt-4 text-center text-muted small"> <p class="mt-4 text-center text-muted small">
Belum punya akun? Kembali ke <a href="/" class="fw-semibold text-decoration-none">halaman utama</a>.
<a href="{{ route('register') }}" class="fw-semibold text-decoration-none">Daftar sekarang</a>
</p> </p>
</form> </form>
<script>
document.addEventListener('DOMContentLoaded', function() {
const togglePassword = document.querySelector('#togglePassword');
const passwordInput = document.querySelector('#password');
const icon = togglePassword.querySelector('i');
togglePassword.addEventListener('click', function() {
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
passwordInput.setAttribute('type', type);
if (type === 'password') {
icon.classList.remove('bi-eye-fill');
icon.classList.add('bi-eye-slash-fill');
} else {
icon.classList.remove('bi-eye-slash-fill');
icon.classList.add('bi-eye-fill');
}
});
});
</script>
</x-guest-layout> </x-guest-layout>

View File

@ -1,6 +1,6 @@
@section('page-title', 'Dashboard') @section('page-title', 'Dashboard')
<x-app-layout> <x-app-layout>
{{-- <button type="button" class="btn btn-sm btn-primary end-0"> {{-- <button type="button" class="btn btn-sm btn-primary end-0">
<i class="bi bi-plus-circle me-1"></i> <i class="bi bi-plus-circle me-1"></i>
Pinjam Buku Baru Pinjam Buku Baru

View File

@ -7,36 +7,63 @@
<title>{{ config('app.name', 'Laravel') }}</title> <title>{{ config('app.name', 'Laravel') }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
@vite(['resources/scss/app.scss', 'resources/js/app.js']) @vite(['resources/scss/app.scss', 'resources/js/app.js'])
<style>
.info-panel {
background-color: var(--bs-primary);
}
.info-svg {
max-width: 400px;
width: 80%;
height: auto;
}
</style>
</head> </head>
<body> <body>
<div class="container-fluid"> <div class="container-fluid">
<div class="row min-vh-100"> <div class="row min-vh-100">
<!-- Left Panel - Info -->
<div class="col-lg-7 d-none d-lg-flex flex-column justify-content-center align-items-center info-panel text-white p-5"> <div class="col-lg-7 d-none d-lg-flex flex-column justify-content-center align-items-center info-panel text-white p-5">
<div class="text-center"> <div class="text-center" style="max-width: 500px;">
<i class="bi bi-book-half" style="font-size: 4rem;"></i> <!-- Logo -->
<h1 class="display-4 fw-bold mt-3">Perpus Digital</h1> <div class="icon-circle bg-white bg-opacity-10 mx-auto mb-4" style="width: 100px; height: 100px; border-radius: 30px;">
<p class="lead mt-3">Gerbang Anda menuju dunia pengetahuan. Jelajahi ribuan koleksi buku digital, pinjam dengan mudah, dan lacak progres membaca Anda.</p> <i class="bi bi-book-half" style="font-size: 3rem;"></i>
</div>
<h1 class="display-4 fw-bold mb-3">DIGIPUS.GO</h1>
<p class="lead opacity-90 mb-4">
Gerbang Anda menuju dunia pengetahuan. Jelajahi ribuan koleksi buku digital, pinjam dengan mudah, dan lacak progres membaca Anda.
</p>
<!-- Features -->
<div class="row g-3 mt-4">
<div class="col-6">
<div class="p-3 bg-white bg-opacity-10 rounded-4">
<i class="bi bi-book fs-3 mb-2 d-block"></i>
<div class="fw-semibold">Ribuan Buku</div>
<small class="opacity-75">Koleksi lengkap</small>
</div>
</div>
<div class="col-6">
<div class="p-3 bg-white bg-opacity-10 rounded-4">
<i class="bi bi-clock-history fs-3 mb-2 d-block"></i>
<div class="fw-semibold">Mudah & Cepat</div>
<small class="opacity-75">Proses instant</small>
</div>
</div>
</div>
</div> </div>
</div> </div>
<!-- Right Panel - Auth Form -->
<div class="col-lg-5 d-flex flex-column justify-content-center align-items-center bg-light p-4"> <div class="col-lg-5 d-flex flex-column justify-content-center align-items-center bg-light p-4">
<div class="card shadow-lg border-0 p-4 p-md-5" style="max-width: 450px; width: 100%;"> <div class="w-100" style="max-width: 450px;">
{{ $slot }} <!-- Mobile Logo -->
<div class="d-lg-none text-center mb-4">
<div class="icon-circle bg-primary-soft mx-auto mb-3" style="width: 70px; height: 70px; border-radius: 22px;">
<i class="bi bi-book-half text-primary" style="font-size: 2rem;"></i>
</div>
<h4 class="fw-bold">DIGIPUS.GO</h4>
</div>
<!-- Auth Card -->
<div class="card shadow-lg border-0" style="border-radius: 28px;">
<div class="card-body p-4 p-md-5">
{{ $slot }}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</body> </body>

View File

@ -4,59 +4,150 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Selamat Datang di {{ config('app.name', 'Perpus Digital') }}</title> <title>Selamat Datang di {{ config('app.name', 'Perpus Digital') }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
@vite(['resources/scss/app.scss', 'resources/js/app.js']) @vite(['resources/scss/app.scss', 'resources/js/app.js'])
<style>
.info-panel {
background-color: var(--bs-primary);
}
.info-svg {
max-width: 400px;
width: 80%;
height: auto;
}
</style>
</head> </head>
<body> <body>
<!-- Hero Section -->
<div class="container-fluid"> <div class="hero-gradient min-vh-100 d-flex align-items-center">
<div class="row min-vh-100"> <div class="container py-5">
<div class="row align-items-center g-5">
<div class="col-lg-7 d-none d-lg-flex flex-column justify-content-center align-items-center info-panel text-white p-5"> <!-- Left Content -->
<div class="text-center"> <div class="col-lg-6">
<i class="bi bi-book-half" style="font-size: 4rem;"></i> <div class="text-white mb-4 mb-lg-0">
<h1 class="display-4 fw-bold mt-3">Perpus Digital</h1> <div class="badge bg-white text-primary px-3 py-2 mb-3" style="border-radius: 50px;">
<p class="lead mt-3">Gerbang Anda menuju dunia pengetahuan. Jelajahi ribuan koleksi buku digital, pinjam dengan mudah, dan lacak progres membaca Anda.</p> <i class="bi bi-stars me-1"></i> Perpustakaan Digital
</div>
<h1 class="display-4 fw-bold mb-3">DIGIPUS.GO</h1>
<p class="fs-5 mb-4 opacity-75">
Gerbang menuju dunia pengetahuan tanpa batas. Akses ribuan koleksi buku digital dengan mudah.
</p>
<div class="row g-3 mt-4">
<div class="col-sm-6">
<div class="d-flex align-items-center gap-3">
<div class="icon-box bg-white bg-opacity-10 text-white" style="border-radius: 16px;">
<i class="bi bi-book"></i>
</div>
<div>
<div class="fw-semibold">Ribuan Buku</div>
<small class="opacity-75">Koleksi lengkap</small>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="d-flex align-items-center gap-3">
<div class="icon-box bg-white bg-opacity-10 text-white" style="border-radius: 16px;">
<i class="bi bi-clock"></i>
</div>
<div>
<div class="fw-semibold">Akses 24/7</div>
<small class="opacity-75">Kapan saja</small>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="d-flex align-items-center gap-3">
<div class="icon-box bg-white bg-opacity-10 text-white" style="border-radius: 16px;">
<i class="bi bi-phone"></i>
</div>
<div>
<div class="fw-semibold">Responsive</div>
<small class="opacity-75">Semua perangkat</small>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="d-flex align-items-center gap-3">
<div class="icon-box bg-white bg-opacity-10 text-white" style="border-radius: 16px;">
<i class="bi bi-shield-check"></i>
</div>
<div>
<div class="fw-semibold">Aman</div>
<small class="opacity-75">Data terlindungi</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Right Content - Login Card -->
<div class="col-lg-5 offset-lg-1">
<div class="card border-0 shadow-lg" style="border-radius: 32px;">
<div class="card-body p-5">
<!-- Logo -->
<div class="text-center mb-4">
<div class="icon-circle bg-primary-soft mx-auto mb-3" style="width: 90px; height: 90px; border-radius: 28px;">
<i class="bi bi-book-half text-primary" style="font-size: 2.5rem;"></i>
</div>
<h3 class="fw-bold mb-2">Selamat Datang</h3>
<p class="text-muted mb-0">Pilih peran Anda untuk melanjutkan</p>
</div>
<!-- Role Options -->
<div class="d-grid gap-3 mb-4">
<!-- Siswa -->
<a href="{{ route('login', ['role' => 'siswa']) }}" class="role-card text-decoration-none">
<div class="card h-100 border-0 bg-light">
<div class="card-body p-4">
<div class="d-flex align-items-center gap-3">
<div class="icon-circle bg-primary text-white flex-shrink-0" style="border-radius: 20px;">
<i class="bi bi-person-badge fs-4"></i>
</div>
<div class="flex-grow-1">
<h5 class="mb-1 fw-bold">Siswa</h5>
<p class="mb-0 text-muted small">Pinjam dan baca buku digital</p>
</div>
<i class="bi bi-arrow-right-circle fs-3 text-primary"></i>
</div>
</div>
</div>
</a>
<!-- Guru -->
<a href="{{ route('login', ['role' => 'guru']) }}" class="role-card text-decoration-none">
<div class="card h-100 border-0 bg-light">
<div class="card-body p-4">
<div class="d-flex align-items-center gap-3">
<div class="icon-circle bg-success text-white flex-shrink-0" style="border-radius: 20px;">
<i class="bi bi-person-workspace fs-4"></i>
</div>
<div class="flex-grow-1">
<h5 class="mb-1 fw-bold">Guru</h5>
<p class="mb-0 text-muted small">Kelola dan rekomendasikan buku</p>
</div>
<i class="bi bi-arrow-right-circle fs-3 text-success"></i>
</div>
</div>
</div>
</a>
</div>
<!-- Divider -->
<div class="text-center mb-3">
<span class="text-muted small">atau</span>
</div>
<!-- Admin Link -->
<div class="text-center">
<a href="{{ route('admin.login') }}" class="btn btn-outline-secondary" style="border-radius: 16px;">
<i class="bi bi-shield-lock me-2"></i>Login sebagai Petugas
</a>
</div>
</div>
</div>
<svg class="info-svg mt-4" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"> <!-- Footer Info -->
<path fill="#FFFFFF" d="M37.3,-45.1C51.2,-36.8,67.3,-26.1,72.4,-11.3C77.5,3.6,71.6,22.6,61,35.1C50.4,47.6,35.1,53.6,19.9,56.9C4.8,60.2,-10.2,60.8,-25.1,55.9C-40,51,-54.8,40.6,-64.3,26.3C-73.8,12,-78,-6.2,-73,-21.2C-68,-36.2,-53.8,-48.1,-39.3,-56.3C-24.8,-64.5,-10,-69,5.7,-68.1C21.4,-67.2,42.8,-60.9,37.3,-45.1" transform="translate(100 100)" style="opacity: 0.1;"></path> <div class="text-center mt-3">
</svg> <small class="text-white opacity-75">
</div> <i class="bi bi-info-circle me-1"></i>
</div> Butuh bantuan? Hubungi petugas perpustakaan
</small>
<div class="col-lg-5 d-flex flex-column justify-content-center align-items-center bg-light p-4">
<div class="card shadow-lg border-0 p-4 p-md-5" style="max-width: 450px; width: 100%;">
<div class="text-center mb-4">
<h3 class="fw-bold text-primary">Selamat Datang</h3>
<p class="text-muted">Silakan pilih peran Anda untuk masuk.</p>
</div>
<div class="d-grid gap-3">
<a href="{{ route('login') }}" class="btn btn-primary btn-lg">
<i class="bi bi-person-badge me-2"></i> Login sebagai Murid
</a>
<a href="{{ route('login') }}" class="btn btn-success btn-lg">
<i class="bi bi-person-workspace me-2"></i> Login sebagai Guru
</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>

View File

@ -1,24 +1,42 @@
<?php <?php
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
// General Controllers
use App\Http\Controllers\DashboardController; use App\Http\Controllers\DashboardController;
use App\Http\Controllers\KatalogController; use App\Http\Controllers\KatalogController;
use App\Http\Controllers\PeminjamanController; use App\Http\Controllers\PeminjamanController;
use App\Http\Controllers\BacaOnlineController; use App\Http\Controllers\BacaOnlineController;
use App\Http\Controllers\RiwayatController; use App\Http\Controllers\RiwayatController;
use App\Http\Controllers\ProfileController; use App\Http\Controllers\ProfileController;
// Admin Controllers
use App\Http\Controllers\Admin\DashboardController as AdminDashboardController; use App\Http\Controllers\Admin\DashboardController as AdminDashboardController;
use App\Http\Controllers\Admin\BookController as AdminBookController; use App\Http\Controllers\Admin\BookController as AdminBookController;
use App\Http\Controllers\Admin\PengumumanController;
use App\Http\Controllers\Admin\UserController as AdminUserController; use App\Http\Controllers\Admin\UserController as AdminUserController;
use App\Http\Controllers\Admin\PengumumanController as AdminPengumumanController;
// Guru Controller
use App\Http\Controllers\Guru\LaporanController; use App\Http\Controllers\Guru\LaporanController;
// Auth Controllers
use App\Http\Controllers\Auth\AdminLoginController;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
*/
// RUTE PUBLIK (Bisa diakses tanpa login)
Route::get('/', function () { Route::get('/', function () {
return view('welcome'); return view('welcome');
}); });
// --- RUTE UNTUK PENGGUNA TERAUTENTIKASI (SISWA & PENJAGA PERPUS) --- // --- RUTE UNTUK PENGGUNA TERAUTENTIKASI (SISWA, GURU, & PENJAGA PERPUS) ---
Route::middleware(['session.auth'])->group(function () { Route::middleware(['auth'])->group(function () {
// Rute Umum untuk Siswa & Guru
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::get('/katalog', [KatalogController::class, 'index'])->name('katalog.index'); Route::get('/katalog', [KatalogController::class, 'index'])->name('katalog.index');
@ -35,39 +53,48 @@
Route::get('/{id}/request', [BacaOnlineController::class, 'showCodeRequestPage'])->name('request_code'); Route::get('/{id}/request', [BacaOnlineController::class, 'showCodeRequestPage'])->name('request_code');
Route::post('/{id}/verify', [BacaOnlineController::class, 'verifyCode'])->name('verify_code'); Route::post('/{id}/verify', [BacaOnlineController::class, 'verifyCode'])->name('verify_code');
Route::get('/{id}/view', [BacaOnlineController::class, 'viewBook'])->name('view_book'); Route::get('/{id}/view', [BacaOnlineController::class, 'viewBook'])->name('view_book');
Route::get('/secure-pdf/{id}', [BacaOnlineController::class, 'streamPdf'])->name('stream_pdf');
}); });
Route::get('/riwayat/offline', [RiwayatController::class, 'offlineIndex'])->name('riwayat.offline'); Route::prefix('riwayat')->name('riwayat.')->group(function () {
Route::get('/riwayat/online', [RiwayatController::class, 'onlineIndex'])->name('riwayat.online'); Route::get('/offline', [RiwayatController::class, 'offlineIndex'])->name('offline');
Route::get('/online', [RiwayatController::class, 'onlineIndex'])->name('online');
});
Route::get('/secure-pdf/{id}', [BacaOnlineController::class, 'streamPdf'])->name('baca.stream_pdf');
// --- Manajemen Profil Pengguna ---
Route::prefix('profile')->name('profile.')->group(function () { Route::prefix('profile')->name('profile.')->group(function () {
Route::get('/', [ProfileController::class, 'index'])->name('index'); Route::get('/', [ProfileController::class, 'index'])->name('index');
Route::get('/edit', [ProfileController::class, 'edit'])->name('edit'); Route::get('/edit', [ProfileController::class, 'edit'])->name('edit');
Route::patch('/', [ProfileController::class, 'update'])->name('update'); Route::patch('/', [ProfileController::class, 'update'])->name('update');
Route::delete('/', [ProfileController::class, 'destroy'])->name('destroy'); Route::delete('/', [ProfileController::class, 'destroy'])->name('destroy');
}); });
// --- GRUP RUTE KHUSUS UNTUK GURU ---
Route::middleware(['role:guru'])->prefix('guru')->name('guru.')->group(function () {
Route::get('/laporan-minat-baca', [LaporanController::class, 'index'])->name('laporan.index');
});
}); });
// --- GRUP RUTE KHUSUS UNTUK ADMIN / PENJAGA PERPUSTAKAAN --- // --- GRUP RUTE KHUSUS UNTUK ADMIN / PENJAGA PERPUSTAKAAN ---
Route::middleware(['session.auth', 'role:penjaga perpus'])->prefix('admin')->name('admin.')->group(function () { Route::middleware(['auth', 'role:penjaga perpus'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', [AdminDashboardController::class, 'index'])->name('dashboard'); Route::get('/dashboard', [AdminDashboardController::class, 'index'])->name('dashboard');
Route::get('/buku', [AdminBookController::class, 'index'])->name('buku.index'); Route::get('/buku', [AdminBookController::class, 'index'])->name('buku.index');
Route::get('/buku/{id}/edit', [AdminBookController::class, 'edit'])->name('buku.edit');
Route::get('/buku/tambah', [AdminBookController::class, 'create'])->name('buku.create'); Route::get('/buku/tambah', [AdminBookController::class, 'create'])->name('buku.create');
Route::get('/buku/{id}/edit', [AdminBookController::class, 'edit'])->name('buku.edit');
Route::get('/pengguna', [AdminUserController::class, 'index'])->name('pengguna.index'); Route::get('/pengguna', [AdminUserController::class, 'index'])->name('pengguna.index');
Route::get('/pengumuman', [PengumumanController::class, 'index'])->name('pengumuman.index');
Route::get('/pengumuman/tambah', [PengumumanController::class, 'create'])->name('pengumuman.create');
Route::get('/pengumuman/{id}/edit', [PengumumanController::class, 'edit'])->name('pengumuman.edit');
Route::get('/pengguna/tambah', [AdminUserController::class, 'create'])->name('pengguna.create'); Route::get('/pengguna/tambah', [AdminUserController::class, 'create'])->name('pengguna.create');
Route::get('/pengguna/{id}/edit', [AdminUserController::class, 'edit'])->name('pengguna.edit'); Route::get('/pengguna/{id}/edit', [AdminUserController::class, 'edit'])->name('pengguna.edit');
Route::get('/pengumuman', [AdminPengumumanController::class, 'index'])->name('pengumuman.index');
Route::get('/pengumuman/tambah', [AdminPengumumanController::class, 'create'])->name('pengumuman.create');
Route::get('/pengumuman/{id}/edit', [AdminPengumumanController::class, 'edit'])->name('pengumuman.edit');
}); });
// GRUP RUTE KHUSUS UNTUK GURU // --- RUTE LOGIN KHUSUS ADMIN ---
Route::middleware(['role:guru'])->prefix('guru')->name('guru.')->group(function () { Route::middleware('guest')->group(function() {
Route::get('/laporan-minat-baca', [LaporanController::class, 'index'])->name('laporan.index'); Route::get('/admin/login', [AdminLoginController::class, 'create'])->name('admin.login');
Route::post('/admin/login', [AdminLoginController::class, 'store'])->name('admin.login.store');
}); });
require __DIR__ . '/auth.php'; require __DIR__ . '/auth.php';