summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--app/Http/Controllers/FileController.php5
-rw-r--r--app/Http/Middleware/LogVisitors.php29
-rw-r--r--app/Http/Requests/FileUploadRequest.php34
-rw-r--r--app/Models/File.php2
-rw-r--r--app/Providers/AppServiceProvider.php6
-rw-r--r--bootstrap/app.php4
-rw-r--r--config/logging.php7
-rw-r--r--resources/js/chunked-upload.js7
-rw-r--r--resources/views/files/upload.blade.php2
-rw-r--r--resources/views/home-simple.blade.php32
-rw-r--r--resources/views/home.blade.php49
-rw-r--r--resources/views/widgets/dunequotes.blade.php0
-rw-r--r--resources/views/widgets/readme.md5
-rw-r--r--resources/views/widgets/status.blade.php0
-rw-r--r--resources/views/widgets/template_widget.blade.php22
-rw-r--r--routes/web.php4
17 files changed, 186 insertions, 25 deletions
diff --git a/README.md b/README.md
index a2c481a..e894cd8 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ requirements phase 1:
- file names remain the same, unless would be overwrite a different file, in which case append epoch seconds.
- file expiration configurable
- rate limiting per ip address
+- all of the controller routes return either json or html, depending on the request parameter "response_type" ('h' or 'j')
phase 1.5
- there is a gallery page view that presents images and videos in kind of a gallery layout, and a slideshow when user clicks one of them.
@@ -20,7 +21,7 @@ phase 1.5
phase 2:
- can bind to different address through command line or dotfile or env var
- https using letsencrypt key defined at a path defined in config.
-- Database integration to store file metadata
+- Database integration to e file metadata
- File model and controller
- IP address tracking and association with uploads
- Page visit tracking by IP
diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php
index 5d0f09a..2c25d7a 100644
--- a/app/Http/Controllers/FileController.php
+++ b/app/Http/Controllers/FileController.php
@@ -32,11 +32,12 @@ class FileController extends Controller
*/
public function upload(FileUploadRequest $request)
{
+ Log::info('uploading');
try {
$uploadedFiles = [];
$urls = [];
- foreach ($request->file('files') as $uploadedFile) {
+ foreach ($request->file('f') as $uploadedFile) {
$file = $this->storeFile($uploadedFile, $request);
$uploadedFiles[] = $file;
$urls[] = $file->download_url;
@@ -102,6 +103,7 @@ class FileController extends Controller
/**
* Handle individual chunk upload
+ * returns json or htmx, depending on value of request->input('response_type')
*/
public function uploadChunk(Request $request)
{
@@ -173,6 +175,7 @@ class FileController extends Controller
*/
public function completeChunkedUpload(Request $request)
{
+ Log::info('uploading chunked');
$request->validate([
'upload_id' => 'required|string',
'original_filename' => 'required|string',
diff --git a/app/Http/Middleware/LogVisitors.php b/app/Http/Middleware/LogVisitors.php
new file mode 100644
index 0000000..bb95c8c
--- /dev/null
+++ b/app/Http/Middleware/LogVisitors.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Http\Middleware;
+
+use Closure;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Log;
+use Symfony\Component\HttpFoundation\Response;
+
+class LogVisitors
+{
+ /**
+ * Handle an incoming request.
+ *
+ * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
+ */
+ public function handle(Request $request, Closure $next): Response
+ {
+ // Log visitor information to dedicated channel
+ Log::channel('visitors')->info('', [
+ 'ip' => $request->ip(),
+ 'time' => now()->toDateTimeString(),
+ 'route' => $request->path(),
+ 'method' => $request->method(),
+ ]);
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Requests/FileUploadRequest.php b/app/Http/Requests/FileUploadRequest.php
index f4302b8..6b326b3 100644
--- a/app/Http/Requests/FileUploadRequest.php
+++ b/app/Http/Requests/FileUploadRequest.php
@@ -4,6 +4,9 @@ namespace App\Http\Requests;
use App\Rules\MimeTypeRule;
use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Contracts\Validation\Validator;
+use Illuminate\Http\Exceptions\HttpResponseException;
class FileUploadRequest extends FormRequest
{
@@ -26,12 +29,12 @@ class FileUploadRequest extends FormRequest
$config = config('fileupload');
return [
- 'files' => [
+ 'f' => [
'required',
'array',
'max:' . $config['max_files_per_upload'],
],
- 'files.*' => [
+ 'f.*' => [
'required',
'file',
'max:' . $config['max_file_size'], // in KB
@@ -48,12 +51,27 @@ class FileUploadRequest extends FormRequest
$config = config('fileupload');
return [
- 'files.required' => 'Please select at least one file to upload.',
- 'files.array' => 'Invalid file upload format.',
- 'files.max' => 'You can upload a maximum of ' . $config['max_files_per_upload'] . ' files at once.',
- 'files.*.required' => 'One or more files are missing.',
- 'files.*.file' => 'One or more uploads are not valid files.',
- 'files.*.max' => 'One or more files exceed the maximum size of ' . $config['max_file_size'] . 'KB.',
+ 'f.required' => 'Please select at least one file to upload.',
+ 'f.array' => 'Invalid file upload format.',
+ 'f.max' => 'You can upload a maximum of ' . $config['max_files_per_upload'] . ' files at once.',
+ 'f.*.required' => 'One or more files are missing.',
+ 'f.*.file' => 'One or more uploads are not valid files.',
+ 'f.*.max' => 'One or more files exceed the maximum size of ' . $config['max_file_size'] . 'KB.',
];
}
+
+ /**
+ * Handle a failed validation attempt.
+ */
+ protected function failedValidation(Validator $validator)
+ {
+ Log::warning('File upload validation failed', [
+ 'ip' => $this->ip(),
+ 'errors' => $validator->errors()->toArray(),
+ 'has_files' => $this->hasFile('files'),
+ 'all_input' => array_keys($this->all()),
+ ]);
+
+ parent::failedValidation($validator);
+ }
}
diff --git a/app/Models/File.php b/app/Models/File.php
index a5a14e3..887333a 100644
--- a/app/Models/File.php
+++ b/app/Models/File.php
@@ -90,7 +90,7 @@ class File extends Model
$days = $days ?? config('fileupload.expiration_days');
if (config('fileupload.expiration_enabled')) {
- $this->expires_at = now()->addDays($days);
+ $this->expires_at = now()->addDays((int) $days);
$this->save();
}
}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 0b1ef2f..6fd3a15 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -29,6 +29,12 @@ class AppServiceProvider extends ServiceProvider
return Limit::perMinute($config['max_attempts'])
->by($request->ip())
->response(function (Request $request, array $headers) {
+ \Log::warning('Rate limit exceeded', [
+ 'ip' => $request->ip(),
+ 'url' => $request->fullUrl(),
+ 'retry_after' => $headers['Retry-After'] ?? 60
+ ]);
+
return response()->json([
'error' => 'Too many upload attempts. Please try again later.',
'retry_after' => $headers['Retry-After'] ?? 60
diff --git a/bootstrap/app.php b/bootstrap/app.php
index c183276..92455e0 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -11,7 +11,9 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
- //
+ $middleware->web(append: [
+ \App\Http\Middleware\LogVisitors::class,
+ ]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
diff --git a/config/logging.php b/config/logging.php
index 9e998a4..057086a 100644
--- a/config/logging.php
+++ b/config/logging.php
@@ -73,6 +73,13 @@ return [
'replace_placeholders' => true,
],
+ 'visitors' => [
+ 'driver' => 'single',
+ 'path' => storage_path('logs/visitors.log'),
+ 'level' => 'info',
+ 'replace_placeholders' => true,
+ ],
+
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
diff --git a/resources/js/chunked-upload.js b/resources/js/chunked-upload.js
index 7688f01..c502187 100644
--- a/resources/js/chunked-upload.js
+++ b/resources/js/chunked-upload.js
@@ -74,12 +74,13 @@ class ChunkedUploader {
async directUpload(file) {
const formData = new FormData();
- formData.append('files[]', file);
+ formData.append('f[]', file);
- const response = await axios.post('/files/upload', formData, {
+ const response = await axios.post('/f/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
- 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
+ 'Accept': 'application/json'
}
});
diff --git a/resources/views/files/upload.blade.php b/resources/views/files/upload.blade.php
index 41cf933..7d5fdd2 100644
--- a/resources/views/files/upload.blade.php
+++ b/resources/views/files/upload.blade.php
@@ -47,7 +47,7 @@
<strong>Choose Files</strong>
</label>
<input type="file"
- name="files[]"
+ name="f[]"
id="files"
multiple
accept=".{{ implode(',.', $allowedExtensions) }}"
diff --git a/resources/views/home-simple.blade.php b/resources/views/home-simple.blade.php
new file mode 100644
index 0000000..b5e4a3d
--- /dev/null
+++ b/resources/views/home-simple.blade.php
@@ -0,0 +1,32 @@
+@extends('template')
+@section('scripts')
+<script type = "module" src = "/js/main.js"></script>
+@endsection
+@section('content')
+
+<section>
+ <h5>Shared Files</h5>
+ <p>Welcome</p>
+ <center> . . . </center>
+</section>
+<section id = "fileupload-js">
+</section>
+<section id = "fileupload-nojs">
+ <div class = "widget" >
+ <h4>File Upload</h4>
+ <h6><a href = "/f">Browse Uploaded Files</a></h6>
+ <form action="/f" method="POST" enctype="multipart/form-data">
+ @csrf
+ @error('f') {{ $message }} @enderror
+ <input multiple name="f[]" type="file">
+ <input hidden name = "response_format" value = "html" />
+ <button type = "submit">Upload</button>
+ </form>
+ </div>
+</section>
+
+<footer>
+ <p>Last updated December 28th, 2025</p>
+</footer>
+@endsection
+</html>
diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php
index b5e4a3d..57c782c 100644
--- a/resources/views/home.blade.php
+++ b/resources/views/home.blade.php
@@ -1,23 +1,25 @@
@extends('template')
@section('scripts')
<script type = "module" src = "/js/main.js"></script>
+<script src = "js/hn.js" defer></script>
@endsection
@section('content')
+<canvas id = "bg">
+</canvas>
+
<section>
- <h5>Shared Files</h5>
+ <h5>December 2025</h5>
<p>Welcome</p>
<center> . . . </center>
</section>
-<section id = "fileupload-js">
-</section>
<section id = "fileupload-nojs">
<div class = "widget" >
<h4>File Upload</h4>
- <h6><a href = "/f">Browse Uploaded Files</a></h6>
- <form action="/f" method="POST" enctype="multipart/form-data">
+ <h6><a href = "/f/browse">Browse Uploaded Files</a></h6>
+ <form action="/f/upload" method="POST" enctype="multipart/form-data">
@csrf
- @error('f') {{ $message }} @enderror
+ @error('upload') {{ $message }} @enderror
<input multiple name="f[]" type="file">
<input hidden name = "response_format" value = "html" />
<button type = "submit">Upload</button>
@@ -25,8 +27,41 @@
</div>
</section>
+<section>
+ <div class="widget">
+ <div class="widget-header">
+ <h4>Hacker News</h4>
+ <button class="refresh-btn" onclick="refreshHackerNews()">↻</button>
+ </div>
+ <div class="search-container">
+ <input
+ type="text"
+ class="search-input"
+ id="hn-search-input"
+ placeholder="Search Hacker News..."
+ onkeypress="if(event.key === 'Enter') searchHackerNews()"
+ >
+ <button class="search-btn" onclick="searchHackerNews()">Search</button>
+ <button class="clear-search-btn" onclick="clearHNSearch()" id="hn-clear-btn" style="display: none;">Clear</button>
+ </div>
+ <div class="sort-container">
+ <button class="sort-btn active" onclick="setHNSort('top')" data-sort="top">Top</button>
+ <button class="sort-btn" onclick="setHNSort('new')" data-sort="new">New</button>
+ <button class="sort-btn" onclick="setHNSort('best')" data-sort="best">Best</button>
+ <button class="sort-btn" onclick="setHNSort('ask')" data-sort="ask">Ask HN</button>
+ <button class="sort-btn" onclick="setHNSort('show')" data-sort="show">Show HN</button>
+ </div>
+ <div class="widget-content" id="hackernews-feed">
+ <div class="loading">Refresh feed to load hackernews posts</div>
+ </div>
+ </div>
+</section>
+
+<section class = "widget" id = "dunechapterexcerpts" hx-get = "/dq" hx-trigger = "load,click" hx-swap = "innerHTML">
+ <h4>random dune pre-chapter quote goes here (under construction)</h4>
+</section>
<footer>
- <p>Last updated December 28th, 2025</p>
+ <p>Last updated December 25th, 2025</p>
</footer>
@endsection
</html>
diff --git a/resources/views/widgets/dunequotes.blade.php b/resources/views/widgets/dunequotes.blade.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/resources/views/widgets/dunequotes.blade.php
diff --git a/resources/views/widgets/readme.md b/resources/views/widgets/readme.md
new file mode 100644
index 0000000..93a9882
--- /dev/null
+++ b/resources/views/widgets/readme.md
@@ -0,0 +1,5 @@
+these are "widgets", or "frames", or "view components", little windows of html.
+
+it is assumed that they are being included (transcluded) into some html page where htmx has been included.
+
+each widget will report an error if the proper scripts are not included
diff --git a/resources/views/widgets/status.blade.php b/resources/views/widgets/status.blade.php
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/resources/views/widgets/status.blade.php
diff --git a/resources/views/widgets/template_widget.blade.php b/resources/views/widgets/template_widget.blade.php
new file mode 100644
index 0000000..0673565
--- /dev/null
+++ b/resources/views/widgets/template_widget.blade.php
@@ -0,0 +1,22 @@
+@yield('widget')
+<script type = "text/javascript">
+
+//check to see if the necessary scripts are loaded
+//htmx
+if (htmx == undefined) {
+ console.warning("main.js not loaded");
+}
+//jquery
+if (window.mainLoaded) {
+ console.warning("main.js not loaded");
+}
+//main.js
+if (window.mainLoaded) {
+ console.warning("main.js not loaded");
+}
+
+</script>
+
+<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>
diff --git a/routes/web.php b/routes/web.php
index a0d2ae8..0dfc846 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -4,11 +4,11 @@ use App\Http\Controllers\FileController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
- return view('welcome');
+ return view('home');
});
// File Upload Routes
-Route::prefix('files')->name('files.')->group(function () {
+Route::prefix('f')->name('files.')->group(function () {
// Upload page
Route::get('/upload', [FileController::class, 'showUploadForm'])
->name('upload.form');