diff options
| author | grothedev <grothedev@gmail.com> | 2026-01-09 20:02:23 -0500 |
|---|---|---|
| committer | grothedev <grothedev@gmail.com> | 2026-01-09 20:02:23 -0500 |
| commit | a8446acd5a32f43534260cbe16225cfa975e18f2 (patch) | |
| tree | d0a42bd9cb319758db803878523187978458c157 | |
| parent | 57445d4ccbfe1cb190437c8f6b609fc83723b015 (diff) | |
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | app/Http/Controllers/FileController.php | 5 | ||||
| -rw-r--r-- | app/Http/Middleware/LogVisitors.php | 29 | ||||
| -rw-r--r-- | app/Http/Requests/FileUploadRequest.php | 34 | ||||
| -rw-r--r-- | app/Models/File.php | 2 | ||||
| -rw-r--r-- | app/Providers/AppServiceProvider.php | 6 | ||||
| -rw-r--r-- | bootstrap/app.php | 4 | ||||
| -rw-r--r-- | config/logging.php | 7 | ||||
| -rw-r--r-- | resources/js/chunked-upload.js | 7 | ||||
| -rw-r--r-- | resources/views/files/upload.blade.php | 2 | ||||
| -rw-r--r-- | resources/views/home-simple.blade.php | 32 | ||||
| -rw-r--r-- | resources/views/home.blade.php | 49 | ||||
| -rw-r--r-- | resources/views/widgets/dunequotes.blade.php | 0 | ||||
| -rw-r--r-- | resources/views/widgets/readme.md | 5 | ||||
| -rw-r--r-- | resources/views/widgets/status.blade.php | 0 | ||||
| -rw-r--r-- | resources/views/widgets/template_widget.blade.php | 22 | ||||
| -rw-r--r-- | routes/web.php | 4 |
17 files changed, 186 insertions, 25 deletions
@@ -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'); |
