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.
*/
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
{
$request->authenticate();
$request->authenticate(); // Menjalankan logika di LoginRequest
$request->session()->regenerate();
// Ambil data user dari session
$userData = session('user_data');
// Cek role dan redirect sesuai role
if ($userData && isset($userData['role']) && $userData['role'] === 'penjaga perpus') {
return redirect()->route('admin.dashboard');
// Karena login sudah dijamin benar, cukup arahkan ke dashboard umum
return redirect()->intended(route('dashboard'));
}
// Default redirect ke dashboard siswa
return redirect()->route('dashboard');
}
/**
* Destroy an authenticated session.
*/

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 Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class CheckRole
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next, string $role): Response
public function handle(Request $request, Closure $next, ...$roles): Response
{
$userData = session('user_data');
// Cek apakah user sudah login
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.');
if (!Auth::check() || !in_array(Auth::user()->role, $roles)) {
return redirect()->route('login')
->with('error', 'Akses ditolak. Anda tidak memiliki izin untuk mengakses halaman tersebut.');
}
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;
use App\Models\User;
use App\Services\DummyDataService;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Events\Lockout;
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
{
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
{
// 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 [
'email' => ['required', 'string', 'email'],
'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
{
// Langkah 1: Pastikan pengguna tidak mencoba login terlalu sering (mencegah brute-force).
$this->ensureIsNotRateLimited();
$allSiswa = DummyDataService::getAllSiswa();
$inputNisn = $this->input('nisn');
// Ambil data yang dikirim dari form login.
$roleDariForm = $this->input('role');
$allUsers = DummyDataService::getAllSiswa();
$inputPassword = $this->input('password');
$userArray = null;
$userArray = collect($allSiswa)->firstWhere('nisn', $inputNisn);
// Tentukan field mana yang akan menerima pesan error jika gagal (nisn atau email).
$errorField = $this->filled('nisn') ? 'nisn' : 'email';
// Langkah 2: Cari data pengguna berdasarkan input yang diberikan.
if ($this->filled('nisn')) {
// Jika form diisi dengan 'nisn', cari pengguna berdasarkan 'nisn'.
$userArray = collect($allUsers)->firstWhere('nisn', $this->input('nisn'));
} else {
// Jika tidak, cari pengguna berdasarkan 'email'.
$userArray = collect($allUsers)->firstWhere('email', $this->input('email'));
}
// Langkah 3: Lakukan Pengecekan Kredensial dan Role.
// Cek #1: Apakah pengguna ditemukan DAN password yang dimasukkan cocok?
if ($userArray && $userArray['password'] === $inputPassword) {
// Simpan ke session
session(['user_data' => $userArray]);
// Set redirect intention berdasarkan role
if (isset($userArray['role']) && $userArray['role'] === 'penjaga perpus') {
session()->put('url.intended', route('admin.dashboard'));
}
// 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;
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());
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
{
// Jika percobaan belum melebihi 5 kali, lanjutkan.
if (!RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
// Jika sudah lebih dari 5 kali, lemparkan error 'throttle'.
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'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
{
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,
'nisn' => '1122334455',
'nama_lengkap' => 'Budi Santoso',
'email' => 'budi.santoso@smkn1perpus.sch.id',
'nomor_hp' => '081122334455',
'password' => 'password',
'role' => 'penjaga perpus',
],
@ -54,10 +52,8 @@ public static function getAllSiswa(): array
],
[
'id' => 5,
'nisn' => '2233445566',
'nama_lengkap' => 'Rina Marlina',
'email' => 'rina.marlina@smkn1perpus.sch.id',
'nomor_hp' => '081223344556',
'password' => 'password',
'role' => 'guru',
],

View File

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

View File

@ -317,3 +317,29 @@ $transition: all 0.3s ease;
.book-option[style*="display: none"] {
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-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') }}">
@csrf
<input type="hidden" name="role" value="{{ $role }}">
<div class="text-center mb-4">
<h3 class="fw-bold text-primary">Login Siswa</h3>
<p class="text-muted">Masukan NISN dan kata sandi Anda.</p>
{{-- Judul dinamis --}}
<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>
{{-- Form dinamis --}}
@if($role == 'siswa')
<div class="mb-3">
<label for="nisn" class="form-label">Nomor Induk Siswa Nasional (NISN)</label>
<input id="nisn" class="form-control bg-body-tertiary @error('nisn') is-invalid @enderror"
type="text" name="nisn" value="{{ old('nisn') }}" required autofocus autocomplete="username" />
@error('nisn')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<input id="nisn" class="form-control" type="text" name="nisn" required autofocus />
<x-input-error :messages="$errors->get('nisn')" class="mt-2" /> {{-- <-- Pastikan ini ada --}}
</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">
<label for="password" class="form-label">Kata Sandi</label>
<div class="input-group">
<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>
<input id="password" class="form-control" type="password" name="password" required />
</div>
<div class="d-flex justify-content-end align-items-center mb-3">
@if (Route::has('password.request'))
<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 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">
Belum punya akun?
<a href="{{ route('register') }}" class="fw-semibold text-decoration-none">Daftar sekarang</a>
Kembali ke <a href="/" class="fw-semibold text-decoration-none">halaman utama</a>.
</p>
</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>

View File

@ -7,37 +7,64 @@
<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">
@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>
<body>
<div class="container-fluid">
<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="text-center">
<i class="bi bi-book-half" style="font-size: 4rem;"></i>
<h1 class="display-4 fw-bold mt-3">Perpus Digital</h1>
<p class="lead mt-3">Gerbang Anda menuju dunia pengetahuan. Jelajahi ribuan koleksi buku digital, pinjam dengan mudah, dan lacak progres membaca Anda.</p>
<div class="text-center" style="max-width: 500px;">
<!-- Logo -->
<div class="icon-circle bg-white bg-opacity-10 mx-auto mb-4" style="width: 100px; height: 100px; border-radius: 30px;">
<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>
<!-- 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="card shadow-lg border-0 p-4 p-md-5" style="max-width: 450px; width: 100%;">
<div class="w-100" style="max-width: 450px;">
<!-- 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>
</body>
</html>

View File

@ -4,59 +4,150 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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">
@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>
<body>
<!-- Hero Section -->
<div class="hero-gradient min-vh-100 d-flex align-items-center">
<div class="container py-5">
<div class="row align-items-center g-5">
<!-- Left Content -->
<div class="col-lg-6">
<div class="text-white mb-4 mb-lg-0">
<div class="badge bg-white text-primary px-3 py-2 mb-3" style="border-radius: 50px;">
<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="container-fluid">
<div class="row min-vh-100">
<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">
<i class="bi bi-book-half" style="font-size: 4rem;"></i>
<h1 class="display-4 fw-bold mt-3">Perpus Digital</h1>
<p class="lead mt-3">Gerbang Anda menuju dunia pengetahuan. Jelajahi ribuan koleksi buku digital, pinjam dengan mudah, dan lacak progres membaca Anda.</p>
<svg class="info-svg mt-4" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<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>
</svg>
<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>
<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%;">
<!-- 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">
<h3 class="fw-bold text-primary">Selamat Datang</h3>
<p class="text-muted">Silakan pilih peran Anda untuk masuk.</p>
<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>
<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
<!-- 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>
<a href="{{ route('login') }}" class="btn btn-success btn-lg">
<i class="bi bi-person-workspace me-2"></i> Login sebagai Guru
<!-- 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>
<!-- Footer Info -->
<div class="text-center mt-3">
<small class="text-white opacity-75">
<i class="bi bi-info-circle me-1"></i>
Butuh bantuan? Hubungi petugas perpustakaan
</small>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1,24 +1,42 @@
<?php
use Illuminate\Support\Facades\Route;
// General Controllers
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\KatalogController;
use App\Http\Controllers\PeminjamanController;
use App\Http\Controllers\BacaOnlineController;
use App\Http\Controllers\RiwayatController;
use App\Http\Controllers\ProfileController;
// Admin Controllers
use App\Http\Controllers\Admin\DashboardController as AdminDashboardController;
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\PengumumanController as AdminPengumumanController;
// Guru Controller
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 () {
return view('welcome');
});
// --- RUTE UNTUK PENGGUNA TERAUTENTIKASI (SISWA & PENJAGA PERPUS) ---
Route::middleware(['session.auth'])->group(function () {
// --- RUTE UNTUK PENGGUNA TERAUTENTIKASI (SISWA, GURU, & PENJAGA PERPUS) ---
Route::middleware(['auth'])->group(function () {
// Rute Umum untuk Siswa & Guru
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::get('/katalog', [KatalogController::class, 'index'])->name('katalog.index');
@ -35,39 +53,48 @@
Route::get('/{id}/request', [BacaOnlineController::class, 'showCodeRequestPage'])->name('request_code');
Route::post('/{id}/verify', [BacaOnlineController::class, 'verifyCode'])->name('verify_code');
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::get('/riwayat/online', [RiwayatController::class, 'onlineIndex'])->name('riwayat.online');
Route::prefix('riwayat')->name('riwayat.')->group(function () {
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::get('/', [ProfileController::class, 'index'])->name('index');
Route::get('/edit', [ProfileController::class, 'edit'])->name('edit');
Route::patch('/', [ProfileController::class, 'update'])->name('update');
Route::delete('/', [ProfileController::class, 'destroy'])->name('destroy');
});
});
// --- GRUP RUTE KHUSUS UNTUK ADMIN / PENJAGA PERPUSTAKAAN ---
Route::middleware(['session.auth', 'role:penjaga perpus'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', [AdminDashboardController::class, 'index'])->name('dashboard');
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('/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/{id}/edit', [AdminUserController::class, 'edit'])->name('pengguna.edit');
});
// GRUP RUTE KHUSUS UNTUK GURU
// --- 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 ---
Route::middleware(['auth', 'role:penjaga perpus'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', [AdminDashboardController::class, 'index'])->name('dashboard');
Route::get('/buku', [AdminBookController::class, 'index'])->name('buku.index');
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/tambah', [AdminUserController::class, 'create'])->name('pengguna.create');
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');
});
// --- RUTE LOGIN KHUSUS ADMIN ---
Route::middleware('guest')->group(function() {
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';