summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgrothedev <grothedev@gmail.com>2026-03-06 12:10:13 -0600
committergrothedev <grothedev@gmail.com>2026-03-06 12:10:13 -0600
commited4b7b02653c6653cad9785fb16f39fcd41eb030 (patch)
tree37c521b24ef3b4ea3dcb7a5107a86c47b9b9e1e6
parentd0304c13cbd5671c8020a142ec8c504d8a29b63c (diff)
rate limiting, some authorization for some routes
-rw-r--r--app/Http/Controllers/DashboardController.php27
-rw-r--r--app/Http/Controllers/FileController.php9
-rw-r--r--app/Http/Controllers/LinkController.php4
-rw-r--r--app/Http/Controllers/WritingController.php6
-rw-r--r--app/Http/Middleware/Admin.php21
-rw-r--r--app/Policies/FilePolicy.php49
-rw-r--r--app/Policies/LinkPolicy.php49
-rw-r--r--app/Policies/WritingPolicy.php49
-rwxr-xr-xapp/Providers/AppServiceProvider.php18
-rwxr-xr-xpackage.json6
-rw-r--r--resources/js/app.js15
-rw-r--r--resources/views/dashboard.blade.php88
-rwxr-xr-xresources/views/home.blade.php2
-rw-r--r--resources/views/marked.blade.php5
-rwxr-xr-xresources/views/template.blade.php6
-rw-r--r--resources/views/writings/create.blade.php1
-rwxr-xr-xroutes/web.php10
-rwxr-xr-xvite.config.js15
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,
}),
],