admin middleware + llms.txt

This commit is contained in:
Doar Mihai
2026-05-02 00:04:55 +03:00
parent 99d2cc9a25
commit b92e315931
9 changed files with 240 additions and 20 deletions
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class DashboardController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
$savedJobs = $user->savedJobs()->with(['category', 'location'])->latest('user_saved_jobs.created_at')->get();
$jobAlerts = $user->jobAlerts()->latest()->get();
return view('dashboard', compact('user', 'savedJobs', 'jobAlerts'));
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureIsAdmin
{
public function handle(Request $request, Closure $next): Response
{
if (! $request->user() || ! $request->user()->is_admin) {
abort(403);
}
return $next($request);
}
}
+2
View File
@@ -23,6 +23,7 @@ class User extends Authenticatable
'name',
'email',
'password',
'is_admin',
];
/**
@@ -35,6 +36,7 @@ class User extends Authenticatable
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_admin' => 'boolean',
];
}
+3 -1
View File
@@ -18,7 +18,9 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
//
$middleware->alias([
'admin' => \App\Http\Middleware\EnsureIsAdmin::class,
]);
})
->withSchedule(function (Schedule $schedule): void {
// =========================================================================
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false)->after('email_verified_at');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_admin');
});
}
};
+63
View File
@@ -0,0 +1,63 @@
# JobifyGarden
> JobifyGarden is a job aggregation platform that collects listings from multiple external job APIs and displays them in one searchable, filterable interface. It is designed to help job seekers find remote and on-site opportunities across all categories without visiting multiple job boards.
## What JobifyGarden Does
JobifyGarden aggregates job listings from external APIs, enriches them with categories and experience levels, and deduplicates them automatically. Users can search by keyword, filter by location, category, job type, experience level, salary range, and recency. Registered users can bookmark jobs and set up job alerts. A blog section publishes career-related articles.
## Public Pages
- **Home** (`/`) — Featured jobs, search bar, category and job type shortcuts.
- **Browse Jobs** (`/jobs`) — Paginated job listings with sidebar filters (category, job type, experience level, salary, date posted) and sort options.
- **Remote Jobs** (`/jobs/remote`) — Listings filtered to remote-eligible positions.
- **Jobs by Category** (`/jobs/category/{slug}`) — Listings scoped to a specific category.
- **Job Detail** (`/jobs/{slug}`) — Full job description, company, location, salary, type, experience level, posting date, apply button, social share, and similar jobs.
- **Blog** (`/blog`) — List of published career articles.
- **Article** (`/blog/{slug}`) — Full article view.
- **About** (`/about`) — Information about the platform.
- **Contact** (`/contact`) — Contact form. Inquiries answered within 24–48 hours.
- **Privacy Policy** (`/privacy`) — Data collection and usage practices.
- **Terms of Service** (`/terms`) — Usage rules and legal terms.
## Job Listings
Each job listing includes:
- **Title** — Position name.
- **Company** — Hiring organisation.
- **Location** — City/region or "Remote".
- **Job Type** — Full-time, part-time, contract, or freelance.
- **Experience Level** — Junior, mid-level, or senior (auto-detected).
- **Salary** — Min/max range with currency when available.
- **Remote** — Whether remote work is allowed.
- **Posted Date** — When the listing was originally posted.
- **Description** — Full HTML job description from the source.
- **Apply URL** — Direct link to the application page on the original job board.
- **Category** and **Tags** — Used for filtering and discovery.
## Blog / Articles
Articles have a title, excerpt, full body, and publication date. Only published articles are publicly visible. Topics are career and job-search focused.
## User Features (Authenticated)
- **Dashboard** (`/dashboard`) — Overview of saved jobs and job alerts.
- **Bookmarks** (`/bookmarks`) — Saved job listings.
- **Profile** (`/profile`) — Account management.
- **Job Alerts** — Saved searches that notify users of new matching jobs.
## Contact
- General: hello@jobifygarden.com
- Support: support@jobifygarden.com
- Privacy: privacy@jobifygarden.com
- Legal: legal@jobifygarden.com
## Technical Notes
- Built with Laravel (PHP), Blade templates, Tailwind CSS, Alpine.js.
- Jobs are fetched automatically every 2 hours from external APIs.
- Deduplication runs daily; expired listings are cleaned after 45 days.
- Sitemaps available at `/sitemap.xml`, `/sitemap-jobs.xml`, `/sitemap-blog.xml`, `/sitemap-main.xml`.
- Robots rules: `/robots.txt`.
+94 -14
View File
@@ -1,17 +1,97 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
@extends('layouts.app')
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900 dark:text-gray-100">
{{ __("You're logged in!") }}
</div>
</div>
@section('title', 'Dashboard - JobifyGarden')
@section('header')
<h1 class="text-xl font-semibold text-gray-900">My Dashboard</h1>
@endsection
@section('content')
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{{-- Welcome --}}
<div class="mb-8">
<h2 class="text-2xl font-bold text-gray-900">Welcome back, {{ $user->name }}</h2>
<p class="mt-1 text-sm text-gray-500">Here's a summary of your activity on JobifyGarden.</p>
</div>
{{-- Stats --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<p class="text-sm font-medium text-gray-500">Saved Jobs</p>
<p class="mt-2 text-3xl font-bold text-emerald-600">{{ $savedJobs->count() }}</p>
</div>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<p class="text-sm font-medium text-gray-500">Job Alerts</p>
<p class="mt-2 text-3xl font-bold text-emerald-600">{{ $jobAlerts->count() }}</p>
</div>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<p class="text-sm font-medium text-gray-500">Member Since</p>
<p class="mt-2 text-xl font-bold text-gray-700">{{ $user->created_at->format('M Y') }}</p>
</div>
</div>
</x-app-layout>
{{-- Saved Jobs --}}
<div class="bg-white rounded-lg shadow-sm border border-gray-200 mb-8">
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900">Saved Jobs</h3>
<a href="{{ route('bookmarks') }}" class="text-sm text-emerald-600 hover:text-emerald-700 font-medium">View all</a>
</div>
@if($savedJobs->isEmpty())
<div class="px-6 py-10 text-center">
<svg class="mx-auto h-10 w-10 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-4.5L5 21V5z"/>
</svg>
<p class="mt-3 text-sm text-gray-500">No saved jobs yet. <a href="{{ route('jobs.index') }}" class="text-emerald-600 hover:underline">Browse jobs</a></p>
</div>
@else
<ul class="divide-y divide-gray-100">
@foreach($savedJobs->take(5) as $job)
<li class="px-6 py-4 flex items-center justify-between hover:bg-gray-50">
<div class="min-w-0">
<a href="{{ route('jobs.show', $job->slug) }}" class="text-sm font-medium text-gray-900 hover:text-emerald-600 truncate block">
{{ $job->title }}
</a>
<p class="text-xs text-gray-500 mt-0.5">{{ $job->company }}@if($job->location) &middot; {{ $job->location }}@endif</p>
</div>
@if($job->job_type)
<span class="ml-4 flex-shrink-0 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-100 text-emerald-800">
{{ $job->job_type }}
</span>
@endif
</li>
@endforeach
</ul>
@endif
</div>
{{-- Quick Links --}}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<a href="{{ route('jobs.index') }}" class="bg-white rounded-lg shadow-sm border border-gray-200 p-5 flex items-center gap-4 hover:border-emerald-300 transition-colors">
<div class="flex-shrink-0 h-10 w-10 bg-emerald-50 rounded-lg flex items-center justify-center">
<svg class="h-5 w-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<div>
<p class="text-sm font-medium text-gray-900">Browse Jobs</p>
<p class="text-xs text-gray-500">Find new opportunities</p>
</div>
</a>
<a href="{{ route('profile.edit') }}" class="bg-white rounded-lg shadow-sm border border-gray-200 p-5 flex items-center gap-4 hover:border-emerald-300 transition-colors">
<div class="flex-shrink-0 h-10 w-10 bg-emerald-50 rounded-lg flex items-center justify-center">
<svg class="h-5 w-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
</div>
<div>
<p class="text-sm font-medium text-gray-900">Edit Profile</p>
<p class="text-xs text-gray-500">Update your information</p>
</div>
</a>
</div>
</div>
@endsection
+14 -4
View File
@@ -96,7 +96,11 @@
</svg>
Bookmarks
</a>
<a href="{{ route('admin.dashboard') }}" class="text-sm font-medium text-gray-500 hover:text-gray-700">Dashboard</a>
@if(auth()->user()->is_admin)
<a href="{{ route('admin.dashboard') }}" class="text-sm font-medium text-gray-500 hover:text-gray-700">Admin</a>
@else
<a href="{{ route('dashboard') }}" class="text-sm font-medium text-gray-500 hover:text-gray-700">Dashboard</a>
@endif
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="text-sm font-medium text-gray-500 hover:text-gray-700">Log out</button>
@@ -153,9 +157,15 @@
<a href="{{ route('bookmarks') }}" class="block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700">
Bookmarks
</a>
<a href="{{ route('admin.dashboard') }}" class="block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700">
Dashboard
</a>
@if(auth()->user()->is_admin)
<a href="{{ route('admin.dashboard') }}" class="block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700">
Admin
</a>
@else
<a href="{{ route('dashboard') }}" class="block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700">
Dashboard
</a>
@endif
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="w-full text-left block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700">
+4 -1
View File
@@ -4,6 +4,7 @@ use App\Http\Controllers\Admin\DashboardController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\Web\BlogController;
use App\Http\Controllers\Web\BookmarkController;
use App\Http\Controllers\Web\DashboardController as UserDashboardController;
use App\Http\Controllers\Web\HomeController;
use App\Http\Controllers\Web\JobController;
use App\Http\Controllers\Web\PageController;
@@ -42,6 +43,8 @@ Route::get('/sitemap-blog.xml', [SitemapController::class, 'blogSitemap'])->name
// Authenticated user routes
Route::middleware('auth')->group(function () {
Route::get('/dashboard', [UserDashboardController::class, 'index'])->name('dashboard');
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
@@ -51,7 +54,7 @@ Route::middleware('auth')->group(function () {
});
// Admin routes
Route::prefix('admin')->name('admin.')->middleware('auth')->group(function () {
Route::prefix('admin')->name('admin.')->middleware(['auth', 'admin'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::get('/logs/api', [DashboardController::class, 'apiLogs'])->name('logs.api');
Route::get('/logs/fetch', [DashboardController::class, 'fetchLogs'])->name('logs.fetch');