diff options
| author | grothedev <grothedev@gmail.com> | 2026-03-06 12:10:13 -0600 |
|---|---|---|
| committer | grothedev <grothedev@gmail.com> | 2026-03-06 12:10:13 -0600 |
| commit | ed4b7b02653c6653cad9785fb16f39fcd41eb030 (patch) | |
| tree | 37c521b24ef3b4ea3dcb7a5107a86c47b9b9e1e6 | |
| parent | d0304c13cbd5671c8020a142ec8c504d8a29b63c (diff) | |
rate limiting, some authorization for some routes
| -rw-r--r-- | app/Http/Controllers/DashboardController.php | 27 | ||||
| -rw-r--r-- | app/Http/Controllers/FileController.php | 9 | ||||
| -rw-r--r-- | app/Http/Controllers/LinkController.php | 4 | ||||
| -rw-r--r-- | app/Http/Controllers/WritingController.php | 6 | ||||
| -rw-r--r-- | app/Http/Middleware/Admin.php | 21 | ||||
| -rw-r--r-- | app/Policies/FilePolicy.php | 49 | ||||
| -rw-r--r-- | app/Policies/LinkPolicy.php | 49 | ||||
| -rw-r--r-- | app/Policies/WritingPolicy.php | 49 | ||||
| -rwxr-xr-x | app/Providers/AppServiceProvider.php | 18 | ||||
| -rwxr-xr-x | package.json | 6 | ||||
| -rw-r--r-- | resources/js/app.js | 15 | ||||
| -rw-r--r-- | resources/views/dashboard.blade.php | 88 | ||||
| -rwxr-xr-x | resources/views/home.blade.php | 2 | ||||
| -rw-r--r-- | resources/views/marked.blade.php | 5 | ||||
| -rwxr-xr-x | resources/views/template.blade.php | 6 | ||||
| -rw-r--r-- | resources/views/writings/create.blade.php | 1 | ||||
| -rwxr-xr-x | routes/web.php | 10 | ||||
| -rwxr-xr-x | vite.config.js | 15 |
18 files changed, 250 insertions, 130 deletions
diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 7335629..c104e77 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -16,10 +16,29 @@ class DashboardController extends Controller { $user = Auth::user(); - // You can add any additional data preparation here - // before passing to the view - - return view('dashboard'); + // Storage stats + $usedStorage = $user->getStorageUsed(); + $totalStorage = $user->storage_quota; + $storagePercent = $totalStorage > 0 ? min(100, round(($usedStorage / $totalStorage) * 100)) : 0; + + // File stats + $fileCount = 0; // TODO: update when DB schema supports user->files() + $recentFiles = collect(); + + // Writing stats + $writingCount = $user->writings()->count(); + $recentWritings = $user->writings()->orderBy('created_at', 'desc')->take(3)->get(); + + return view('dashboard', compact( + 'user', + 'usedStorage', + 'totalStorage', + 'storagePercent', + 'fileCount', + 'recentFiles', + 'writingCount', + 'recentWritings', + )); } /** diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php index 5a4ed40..70d07ef 100644 --- a/app/Http/Controllers/FileController.php +++ b/app/Http/Controllers/FileController.php @@ -145,7 +145,8 @@ class FileController extends Controller public function addTags(Request $request, $fileId) { $file = File::findOrFail($fileId); - + $this->authorize('update', $file); + $validated = $request->validate([ 'tags' => 'required|string' ]); @@ -169,6 +170,8 @@ class FileController extends Controller public function removeTag(Request $request, $fileId, $tagId) { $file = File::findOrFail($fileId); + $this->authorize('update', $file); + $tag = Tag::findOrFail($tagId); $file->tags()->detach($tagId); @@ -196,6 +199,8 @@ class FileController extends Controller */ public function update(Request $request, File $file) { + $this->authorize('update', $file); + $validated = $request->validate([ 'description' => 'nullable|string', 'tags' => 'nullable|string' @@ -218,6 +223,8 @@ class FileController extends Controller */ public function destroy(File $file) { + $this->authorize('delete', $file); + // Remove the file from storage if (Storage::disk('public')->exists($file->path)) { Storage::disk('public')->delete($file->path); diff --git a/app/Http/Controllers/LinkController.php b/app/Http/Controllers/LinkController.php index 9ac44d8..e889489 100644 --- a/app/Http/Controllers/LinkController.php +++ b/app/Http/Controllers/LinkController.php @@ -70,6 +70,8 @@ class LinkController extends Controller */ public function update(UpdateLinkRequest $request, Link $link) { + $this->authorize('update', $link); + $validated = $request->validated(); $link->update($validated); @@ -82,6 +84,8 @@ class LinkController extends Controller */ public function destroy(Link $link) { + $this->authorize('delete', $link); + $link->delete(); $redirect = request()->input('_redirect', route('l.index')); diff --git a/app/Http/Controllers/WritingController.php b/app/Http/Controllers/WritingController.php index 974852f..67d7b8d 100644 --- a/app/Http/Controllers/WritingController.php +++ b/app/Http/Controllers/WritingController.php @@ -66,6 +66,8 @@ class WritingController extends Controller public function edit($id) { $writing = Writing::findOrFail($id); + $this->authorize('update', $writing); + return view('writings.edit', [ 'writing' => $writing ]); @@ -76,6 +78,8 @@ class WritingController extends Controller */ public function update(Request $request, Writing $writing) { + $this->authorize('update', $writing); + $validated = $request->validate([ 'title' => 'required|min:3|max:255', 'content' => 'required|min:10', @@ -92,6 +96,8 @@ class WritingController extends Controller */ public function destroy(Writing $writing) { + $this->authorize('delete', $writing); + $writing->delete(); return redirect()->route('w.index') diff --git a/app/Http/Middleware/Admin.php b/app/Http/Middleware/Admin.php index ab70520..65fba8c 100644 --- a/app/Http/Middleware/Admin.php +++ b/app/Http/Middleware/Admin.php @@ -6,6 +6,7 @@ use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Notification; use App\Notifications\AdminAccessNotification; use Illuminate\Notifications\AnonymousNotifiable; @@ -21,16 +22,20 @@ class Admin abort(403, 'Unauthorized'); } - // Notify admin email of access + // Notify admin email of access — throttled to once per 15 minutes per user+IP $adminEmail = config('app.admin_notify_email'); if ($adminEmail) { - Notification::route('mail', $adminEmail)->notify( - new AdminAccessNotification( - Auth::user()->name, - $request->ip(), - $request->header('User-Agent', 'unknown') - ) - ); + $cacheKey = 'admin_notify:' . Auth::id() . ':' . $request->ip(); + if (!Cache::has($cacheKey)) { + Notification::route('mail', $adminEmail)->notify( + new AdminAccessNotification( + Auth::user()->name, + $request->ip(), + $request->header('User-Agent', 'unknown') + ) + ); + Cache::put($cacheKey, true, now()->addMinutes(15)); + } } return $next($request); diff --git a/app/Policies/FilePolicy.php b/app/Policies/FilePolicy.php new file mode 100644 index 0000000..fe46f8b --- /dev/null +++ b/app/Policies/FilePolicy.php @@ -0,0 +1,49 @@ +<?php + +namespace App\Policies; + +use App\Models\File; +use App\Models\User; + +class FilePolicy +{ + /** + * Anyone can view listings. + */ + public function viewAny(?User $user): bool + { + return true; + } + + /** + * Anyone can view a single file. + */ + public function view(?User $user, File $file): bool + { + return true; + } + + /** + * Any authenticated user can create files. + */ + public function create(User $user): bool + { + return true; + } + + /** + * Only the owner or an admin can update. + */ + public function update(User $user, File $file): bool + { + return $user->id === $file->user_id || $user->isAdmin(); + } + + /** + * Only the owner or an admin can delete. + */ + public function delete(User $user, File $file): bool + { + return $user->id === $file->user_id || $user->isAdmin(); + } +} diff --git a/app/Policies/LinkPolicy.php b/app/Policies/LinkPolicy.php new file mode 100644 index 0000000..1eba28f --- /dev/null +++ b/app/Policies/LinkPolicy.php @@ -0,0 +1,49 @@ +<?php + +namespace App\Policies; + +use App\Models\Link; +use App\Models\User; + +class LinkPolicy +{ + /** + * Anyone can view listings. + */ + public function viewAny(?User $user): bool + { + return true; + } + + /** + * Anyone can view a single link. + */ + public function view(?User $user, Link $link): bool + { + return true; + } + + /** + * Any authenticated user can create links. + */ + public function create(User $user): bool + { + return true; + } + + /** + * Only the owner or an admin can update. + */ + public function update(User $user, Link $link): bool + { + return $user->id === $link->user_id || $user->isAdmin(); + } + + /** + * Only the owner or an admin can delete. + */ + public function delete(User $user, Link $link): bool + { + return $user->id === $link->user_id || $user->isAdmin(); + } +} diff --git a/app/Policies/WritingPolicy.php b/app/Policies/WritingPolicy.php new file mode 100644 index 0000000..e2bbaab --- /dev/null +++ b/app/Policies/WritingPolicy.php @@ -0,0 +1,49 @@ +<?php + +namespace App\Policies; + +use App\Models\User; +use App\Models\Writing; + +class WritingPolicy +{ + /** + * Anyone can view listings. + */ + public function viewAny(?User $user): bool + { + return true; + } + + /** + * Anyone can view a single writing. + */ + public function view(?User $user, Writing $writing): bool + { + return true; + } + + /** + * Any authenticated user can create writings. + */ + public function create(User $user): bool + { + return true; + } + + /** + * Only the owner or an admin can update. + */ + public function update(User $user, Writing $writing): bool + { + return $user->id === $writing->user_id || $user->isAdmin(); + } + + /** + * Only the owner or an admin can delete. + */ + public function delete(User $user, Writing $writing): bool + { + return $user->id === $writing->user_id || $user->isAdmin(); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..8eee884 100755 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,9 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Support\Facades\RateLimiter; +use Illuminate\Http\Request; class AppServiceProvider extends ServiceProvider { @@ -19,6 +22,19 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + // Public routes: 60 requests per minute per IP + RateLimiter::for('public', function (Request $request) { + return Limit::perMinute(60)->by($request->ip()); + }); + + // Admin routes: 30 requests per minute per authenticated user + RateLimiter::for('admin', function (Request $request) { + return Limit::perMinute(30)->by($request->user()?->id ?: $request->ip()); + }); + + // Expensive operations (audio stream, file uploads): 5 per minute + RateLimiter::for('expensive', function (Request $request) { + return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip()); + }); } } diff --git a/package.json b/package.json index 08953d8..231dffb 100755 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@tailwindcss/forms": "^0.5.2", "alpinejs": "^3.4.2", "autoprefixer": "^10.4.2", - "axios": "^1.7.4", + "axios": "^1.7.9", "concurrently": "^9.0.1", "laravel-echo": "^1.17.1", "laravel-vite-plugin": "^1.0", @@ -17,5 +17,9 @@ "pusher-js": "^8.4.0-rc2", "tailwindcss": "^3.1.0", "vite": "^5.0" + }, + "dependencies": { + "htmx.org": "^2.0.1", + "jquery": "^3.7.1" } } diff --git a/resources/js/app.js b/resources/js/app.js new file mode 100644 index 0000000..f0fa127 --- /dev/null +++ b/resources/js/app.js @@ -0,0 +1,15 @@ +import htmx from 'htmx.org'; +import axios from 'axios'; +import jQuery from 'jquery'; + +// Expose globally so inline scripts and Blade templates can use them +window.htmx = htmx; +window.axios = axios; +window.$ = jQuery; +window.jQuery = jQuery; + +// Set axios CSRF header from meta tag +const token = document.querySelector('meta[name="csrf-token"]'); +if (token) { + axios.defaults.headers.common['X-CSRF-TOKEN'] = token.getAttribute('content'); +} diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index e73bb2a..e460bde 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -163,36 +163,6 @@ <main> <h1>Dashboard</h1> - @php - $user = Auth::user(); - - // Storage stats - $usedStorage = $user->getStorageUsed(); - $totalStorage = $user->storage_quota; - $storagePercent = $totalStorage > 0 ? min(100, round(($usedStorage / $totalStorage) * 100)) : 0; - - // File stats - #$fileCount = $user->files()->count(); - $fileCount = 0; - #TODO later cause need to change the DB schema - #$recentFiles = $user->files()->orderBy('created_at', 'desc')->take(3)->get(); - $recentFiles = []; - // Writing stats - $writingCount = $user->writings()->count(); - $recentWritings = $user->writings()->orderBy('created_at', 'desc')->take(3)->get(); - - // Quest stats (if implemented) - $questCount = method_exists($user, 'quests') ? $user->quests()->count() : 0; - $activeQuests = method_exists($user, 'quests') ? $user->quests() - ->wherePivot('status', 'in_progress') - ->orderBy('created_at', 'desc') - ->take(3) - ->get() : collect(); - - // Inventory stats (placeholder) - $inventoryCount = 0; // Update when inventory is implemented - @endphp - <!-- Profile Overview --> <div class="dashboard-card" style="margin-bottom: 20px;"> <div class="avatar-container"> @@ -218,14 +188,6 @@ <div class="stat-number">{{ $writingCount }}</div> <div class="stat-label">Writings</div> </div> - <div class="stat-item"> - <div class="stat-number">{{ $questCount }}</div> - <div class="stat-label">Quests</div> - </div> - <div class="stat-item"> - <div class="stat-number">{{ $inventoryCount }}</div> - <div class="stat-label">Items</div> - </div> </div> <div class="dashboard-container"> @@ -281,56 +243,6 @@ <a href="{{ route('w.index') }}">All Writings →</a> </div> </div> - - <!-- Quests Card --> - <div class="dashboard-card"> - <h2>Quests</h2> - - @if(method_exists($user, 'quests') && $activeQuests->count() > 0) - <ul class="item-list"> - @foreach($activeQuests as $quest) - <li> - <div> - <a href="{{ route('quests.show', $quest->id) }}">{{ Str::limit($quest->title, 35) }}</a> - <div style="font-size: 0.8rem; color: #666;"> - Progress: {{ $quest->pivot->progress }}% - </div> - <div class="progress-bar" style="margin: 5px 0; height: 6px;"> - <div class="progress-fill" style="width: {{ $quest->pivot->progress }}%; background-color: #00a86b;"></div> - </div> - <div style="font-size: 0.8rem; color: #666;"> - <span class="badge badge-{{ $quest->difficulty == 'easy' ? 'success' : ($quest->difficulty == 'hard' ? 'warning' : 'primary') }}"> - {{ ucfirst($quest->difficulty) }} - </span> - {{ $quest->reward_points }} points - </div> - </div> - </li> - @endforeach - </ul> - @else - <div class="empty-state">No active quests</div> - @endif - - <div class="dashboard-card-footer"> - @if(method_exists($user, 'quests')) - <a href="{{ route('quests.index') }}">View Quests →</a> - @else - <span style="color: #999;">Coming soon</span> - @endif - </div> - </div> - - <!-- Inventory Card (Placeholder) --> - <div class="dashboard-card"> - <h2>Inventory</h2> - - <div class="empty-state">Inventory system coming soon</div> - - <div class="dashboard-card-footer"> - <span style="color: #999;">Under development</span> - </div> - </div> </div> <!-- Tags Used --> diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index c905d8f..c993212 100755 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -70,7 +70,7 @@ UNDER CONSTRUCTION; APOLOGIES FOR LACKING FUNCTIONALITY. </div> </section> <footer> - <p>Last updated February 14th, 2026</p> + <p>Last updated March 6th, 2026</p> </footer> @endsection </html> diff --git a/resources/views/marked.blade.php b/resources/views/marked.blade.php index 8208ac7..f1deefc 100644 --- a/resources/views/marked.blade.php +++ b/resources/views/marked.blade.php @@ -7,10 +7,9 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!--<link rel="stylesheet" type="text/css" href="static/reset.css">--> <link rel="stylesheet" type="text/css" href="style.css"> - <script src="https://unpkg.com/htmx.org@2.0.1"></script> - <script src = "https://code.jquery.com/jquery-3.7.1.min.js"></script> + @vite('resources/js/app.js') <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> - @vite('resources/js/marked.js') + <script src="/js/marked.js"></script> <!--<base href = "http://192.168.4.32:9002/">--> <!--<base href = "http://localhost:9002/">--> <title>markup playground</title> diff --git a/resources/views/template.blade.php b/resources/views/template.blade.php index 4305656..09c7389 100755 --- a/resources/views/template.blade.php +++ b/resources/views/template.blade.php @@ -30,9 +30,7 @@ <footer> </footer> </main> + @vite('resources/js/app.js') + @yield('scripts') </body> -<script src="https://unpkg.com/htmx.org@2.0.1"></script> -<script src="https://unpkg.com/axios/dist/axios.min.js"></script> -<script src = "https://code.jquery.com/jquery-3.7.1.min.js"></script> -@yield('scripts') </html>
\ No newline at end of file diff --git a/resources/views/writings/create.blade.php b/resources/views/writings/create.blade.php index 54b5deb..86264b9 100644 --- a/resources/views/writings/create.blade.php +++ b/resources/views/writings/create.blade.php @@ -2,7 +2,6 @@ @section('head') <meta charset="UTF-8"> -<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script type = "module" src = "/js/writing_create.js"></script> <link rel="stylesheet" href="/css/style.css"> diff --git a/routes/web.php b/routes/web.php index 8370fd2..d5fe827 100755 --- a/routes/web.php +++ b/routes/web.php @@ -14,7 +14,7 @@ Route::get('/', function () { return view('home'); }); -Route::get('/dq', [SiteController::class, 'duneQuote']); +Route::get('/dq', [SiteController::class, 'duneQuote'])->middleware('throttle:public'); Route::get('/f/{path}', function($path){ // Sanitize: strip directory traversal, only allow basename @@ -28,7 +28,7 @@ Route::get('/f/{path}', function($path){ 'Content-Type' => $mime, 'Content-Disposition' => 'inline; filename="'.$path.'"' ]); -}); +})->middleware('throttle:public'); Route::get('/mu/{path}', function($path){ // Sanitize: strip directory traversal, only allow basename @@ -42,7 +42,7 @@ Route::get('/mu/{path}', function($path){ 'Content-Type' => $mime, 'Content-Disposition' => 'inline; filename="'.$path.'"' ]); -}); +})->middleware('throttle:public'); // Public read-only routes Route::resource('w', WritingController::class)->only(['index', 'show']); @@ -64,7 +64,7 @@ Route::middleware('auth')->group(function () { }); // Admin-only routes -Route::middleware(['auth', \App\Http\Middleware\Admin::class])->group(function () { +Route::middleware(['auth', 'throttle:admin', \App\Http\Middleware\Admin::class])->group(function () { Route::get('/admin', [AdminController::class, 'index'])->name('admin'); Route::post('/l/import', [LinkController::class, 'import'])->name('l.import'); Route::get('/env', [SiteController::class, 'env']); @@ -121,7 +121,7 @@ Route::get('/stream/audio', function() { 'Cache-Control' => 'no-cache', 'X-Accel-Buffering' => 'no' ]); -})->middleware('auth'); +})->middleware(['auth', 'throttle:expensive']); Route::get('/newtab', function() { return view('newtab'); diff --git a/vite.config.js b/vite.config.js index 84c2295..b368fe9 100755 --- a/vite.config.js +++ b/vite.config.js @@ -5,19 +5,8 @@ export default defineConfig({ plugins: [ laravel({ input: [ - 'resources/css/app.css', - 'resources/css/skeleton.css', - 'resources/css/normalize.css', - 'resources/css/style.css', - 'resources/css/writings.css', - 'resources/css/jstoys.css', - 'resources/js/main.js', - 'resources/js/marked.js', - 'resources/js/writing_index.js', - 'resources/js/writing_create.js', - 'resources/js/writing_show.js', - 'resources/js/blood_gpu.js', - 'resources/js/blood.js'], + 'resources/js/app.js', + ], refresh: true, }), ], |
