feat: add guestbook with rate limiting (#6)
* Re-add guestbook w/ rate limiting * Add guestbook to navbar
This commit is contained in:
		
					parent
					
						
							
								1b267f6102
							
						
					
				
			
			
				commit
				
					
						8482a98ca6
					
				
			
		
					 12 changed files with 329 additions and 91 deletions
				
			
		
							
								
								
									
										29
									
								
								app/Http/Controllers/GuestbookController.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								app/Http/Controllers/GuestbookController.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| <?php | ||||
| 
 | ||||
| namespace App\Http\Controllers; | ||||
| 
 | ||||
| use Illuminate\Http\Request; | ||||
| use DB; | ||||
| 
 | ||||
| class GuestbookController extends Controller { | ||||
|     public function guestbook() { | ||||
|         return view('pages.guestbook'); | ||||
|     } | ||||
| 
 | ||||
|     public function guestbookPost(Request $request) { | ||||
|         $this->validate($request, [ | ||||
|             'name' => 'required', | ||||
|             'message' => 'required' | ||||
|         ]); | ||||
| 
 | ||||
|         DB::insert('INSERT INTO guestbook_entries (name, timestamp, ip_address, agent, message) values (?, ?, ?, ?, ?)', array( | ||||
|             htmlspecialchars($request->get('name')), | ||||
|             time(), | ||||
|             $request->ip(), | ||||
|             $request->userAgent(), | ||||
|             htmlspecialchars($request->get('message')) | ||||
|         )); | ||||
| 
 | ||||
|         return back()->with('success', 'Entry submitted successfully!'); | ||||
|     } | ||||
| } | ||||
|  | @ -45,6 +45,11 @@ class Kernel extends HttpKernel | |||
|         ], | ||||
|     ]; | ||||
| 
 | ||||
|     protected $routeMiddleware = [ | ||||
|         'rate_limit' => \App\Http\Middleware\RateLimiter::class, | ||||
|     ]; | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * The application's middleware aliases. | ||||
|      * | ||||
|  |  | |||
							
								
								
									
										32
									
								
								app/Http/Middleware/RateLimiter.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/Http/Middleware/RateLimiter.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| <?php | ||||
| 
 | ||||
| namespace App\Http\Middleware; | ||||
| 
 | ||||
| use Closure; | ||||
| use Illuminate\Http\Request; | ||||
| use Illuminate\Support\Facades\Cache; | ||||
| use Symfony\Component\HttpFoundation\Response; | ||||
| 
 | ||||
| class RateLimiter | ||||
| { | ||||
|     /** | ||||
|      * Handle an incoming request. | ||||
|      * | ||||
|      * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next | ||||
|      */ | ||||
|     public function handle(Request $request, Closure $next): Response | ||||
|     { | ||||
|         $ipAddress = $request->ip(); | ||||
|         $cacheKey = 'rate_limit_' . $ipAddress; | ||||
| 
 | ||||
|         if (Cache::has($cacheKey)) { | ||||
|             // If the cache key exists, the IP has submitted an entry within the last hour
 | ||||
|             return response()->view('errors.ratelimit-guestbook', [], 429); | ||||
|         } | ||||
| 
 | ||||
|         // Add the IP address to the cache and set the expiration time to one hour
 | ||||
|         Cache::put($cacheKey, true, 60); | ||||
| 
 | ||||
|         return $next($request); | ||||
|     } | ||||
| } | ||||
|  | @ -9,7 +9,8 @@ | |||
|         "guzzlehttp/guzzle": "^7.2", | ||||
|         "laravel/framework": "^10.10", | ||||
|         "laravel/sanctum": "^3.2", | ||||
|         "laravel/tinker": "^2.8" | ||||
|         "laravel/tinker": "^2.8", | ||||
|         "spatie/laravel-honeypot": "^4.3" | ||||
|     }, | ||||
|     "require-dev": { | ||||
|         "fakerphp/faker": "^1.9.1", | ||||
|  |  | |||
							
								
								
									
										138
									
								
								composer.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										138
									
								
								composer.lock
									
										
									
										generated
									
									
									
								
							|  | @ -4,7 +4,7 @@ | |||
|         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", | ||||
|         "This file is @generated automatically" | ||||
|     ], | ||||
|     "content-hash": "aa322c53454393ed775cfe4807d54a50", | ||||
|     "content-hash": "505f8d503188864625fc855900ea2202", | ||||
|     "packages": [ | ||||
|         { | ||||
|             "name": "brick/math", | ||||
|  | @ -2994,6 +2994,142 @@ | |||
|             ], | ||||
|             "time": "2023-04-15T23:01:58+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "spatie/laravel-honeypot", | ||||
|             "version": "4.3.2", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/spatie/laravel-honeypot.git", | ||||
|                 "reference": "eab92dd2096f1cdb83c28ced4f4632d3cfde2872" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/spatie/laravel-honeypot/zipball/eab92dd2096f1cdb83c28ced4f4632d3cfde2872", | ||||
|                 "reference": "eab92dd2096f1cdb83c28ced4f4632d3cfde2872", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "illuminate/contracts": "^8.0|^9.0|^10.0", | ||||
|                 "illuminate/encryption": "^8.0|^9.0|^10.0", | ||||
|                 "illuminate/http": "^8.0|^9.0|^10.0", | ||||
|                 "illuminate/support": "^8.0|^9.0|^10.0", | ||||
|                 "illuminate/validation": "^8.0|^9.0|^10.0", | ||||
|                 "nesbot/carbon": "^2.0", | ||||
|                 "php": "^8.0", | ||||
|                 "spatie/laravel-package-tools": "^1.9", | ||||
|                 "symfony/http-foundation": "^5.1.2|^6.0" | ||||
|             }, | ||||
|             "require-dev": { | ||||
|                 "livewire/livewire": "^2.10", | ||||
|                 "orchestra/testbench": "^6.23|^7.0|^8.0", | ||||
|                 "pestphp/pest-plugin-livewire": "^1.0", | ||||
|                 "phpunit/phpunit": "^9.4", | ||||
|                 "spatie/pest-plugin-snapshots": "^1.1", | ||||
|                 "spatie/phpunit-snapshot-assertions": "^4.2", | ||||
|                 "spatie/test-time": "^1.2.1" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "extra": { | ||||
|                 "laravel": { | ||||
|                     "providers": [ | ||||
|                         "Spatie\\Honeypot\\HoneypotServiceProvider" | ||||
|                     ] | ||||
|                 } | ||||
|             }, | ||||
|             "autoload": { | ||||
|                 "psr-4": { | ||||
|                     "Spatie\\Honeypot\\": "src" | ||||
|                 } | ||||
|             }, | ||||
|             "notification-url": "https://packagist.org/downloads/", | ||||
|             "license": [ | ||||
|                 "MIT" | ||||
|             ], | ||||
|             "authors": [ | ||||
|                 { | ||||
|                     "name": "Freek Van der Herten", | ||||
|                     "email": "freek@spatie.be", | ||||
|                     "homepage": "https://spatie.be", | ||||
|                     "role": "Developer" | ||||
|                 } | ||||
|             ], | ||||
|             "description": "Preventing spam submitted through forms", | ||||
|             "homepage": "https://github.com/spatie/laravel-honeypot", | ||||
|             "keywords": [ | ||||
|                 "laravel-honeypot", | ||||
|                 "spatie" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "source": "https://github.com/spatie/laravel-honeypot/tree/4.3.2" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
|                     "url": "https://spatie.be/open-source/support-us", | ||||
|                     "type": "custom" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2023-01-17T07:09:34+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "spatie/laravel-package-tools", | ||||
|             "version": "1.15.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/spatie/laravel-package-tools.git", | ||||
|                 "reference": "efab1844b8826443135201c4443690f032c3d533" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/efab1844b8826443135201c4443690f032c3d533", | ||||
|                 "reference": "efab1844b8826443135201c4443690f032c3d533", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "illuminate/contracts": "^9.28|^10.0", | ||||
|                 "php": "^8.0" | ||||
|             }, | ||||
|             "require-dev": { | ||||
|                 "mockery/mockery": "^1.5", | ||||
|                 "orchestra/testbench": "^7.7|^8.0", | ||||
|                 "pestphp/pest": "^1.22", | ||||
|                 "phpunit/phpunit": "^9.5.24", | ||||
|                 "spatie/pest-plugin-test-time": "^1.1" | ||||
|             }, | ||||
|             "type": "library", | ||||
|             "autoload": { | ||||
|                 "psr-4": { | ||||
|                     "Spatie\\LaravelPackageTools\\": "src" | ||||
|                 } | ||||
|             }, | ||||
|             "notification-url": "https://packagist.org/downloads/", | ||||
|             "license": [ | ||||
|                 "MIT" | ||||
|             ], | ||||
|             "authors": [ | ||||
|                 { | ||||
|                     "name": "Freek Van der Herten", | ||||
|                     "email": "freek@spatie.be", | ||||
|                     "role": "Developer" | ||||
|                 } | ||||
|             ], | ||||
|             "description": "Tools for creating Laravel packages", | ||||
|             "homepage": "https://github.com/spatie/laravel-package-tools", | ||||
|             "keywords": [ | ||||
|                 "laravel-package-tools", | ||||
|                 "spatie" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "issues": "https://github.com/spatie/laravel-package-tools/issues", | ||||
|                 "source": "https://github.com/spatie/laravel-package-tools/tree/1.15.0" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
|                     "url": "https://github.com/spatie", | ||||
|                     "type": "github" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2023-04-27T08:09:01+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/console", | ||||
|             "version": "v6.3.0", | ||||
|  |  | |||
|  | @ -34,15 +34,6 @@ | |||
|     */ | ||||
| 
 | ||||
|     'connections' => [ | ||||
| 
 | ||||
|         'sqlite' => [ | ||||
|             'driver' => 'sqlite', | ||||
|             'url' => env('DATABASE_URL'), | ||||
|             'database' => env('DB_DATABASE', database_path('database.sqlite')), | ||||
|             'prefix' => '', | ||||
|             'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), | ||||
|         ], | ||||
| 
 | ||||
|         'mysql' => [ | ||||
|             'driver' => 'mysql', | ||||
|             'url' => env('DATABASE_URL'), | ||||
|  | @ -62,37 +53,6 @@ | |||
|                 PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), | ||||
|             ]) : [], | ||||
|         ], | ||||
| 
 | ||||
|         'pgsql' => [ | ||||
|             'driver' => 'pgsql', | ||||
|             'url' => env('DATABASE_URL'), | ||||
|             'host' => env('DB_HOST', '127.0.0.1'), | ||||
|             'port' => env('DB_PORT', '5432'), | ||||
|             'database' => env('DB_DATABASE', 'forge'), | ||||
|             'username' => env('DB_USERNAME', 'forge'), | ||||
|             'password' => env('DB_PASSWORD', ''), | ||||
|             'charset' => 'utf8', | ||||
|             'prefix' => '', | ||||
|             'prefix_indexes' => true, | ||||
|             'search_path' => 'public', | ||||
|             'sslmode' => 'prefer', | ||||
|         ], | ||||
| 
 | ||||
|         'sqlsrv' => [ | ||||
|             'driver' => 'sqlsrv', | ||||
|             'url' => env('DATABASE_URL'), | ||||
|             'host' => env('DB_HOST', 'localhost'), | ||||
|             'port' => env('DB_PORT', '1433'), | ||||
|             'database' => env('DB_DATABASE', 'forge'), | ||||
|             'username' => env('DB_USERNAME', 'forge'), | ||||
|             'password' => env('DB_PASSWORD', ''), | ||||
|             'charset' => 'utf8', | ||||
|             'prefix' => '', | ||||
|             'prefix_indexes' => true, | ||||
|             // 'encrypt' => env('DB_ENCRYPT', 'yes'),
 | ||||
|             // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
 | ||||
|         ], | ||||
| 
 | ||||
|     ], | ||||
| 
 | ||||
|     /* | ||||
|  | @ -107,45 +67,4 @@ | |||
|     */ | ||||
| 
 | ||||
|     'migrations' => 'migrations', | ||||
| 
 | ||||
|     /* | ||||
|     |-------------------------------------------------------------------------- | ||||
|     | Redis Databases | ||||
|     |-------------------------------------------------------------------------- | ||||
|     | | ||||
|     | Redis is an open source, fast, and advanced key-value store that also | ||||
|     | provides a richer body of commands than a typical key-value system | ||||
|     | such as APC or Memcached. Laravel makes it easy to dig right in. | ||||
|     | | ||||
|     */ | ||||
| 
 | ||||
|     'redis' => [ | ||||
| 
 | ||||
|         'client' => env('REDIS_CLIENT', 'phpredis'), | ||||
| 
 | ||||
|         'options' => [ | ||||
|             'cluster' => env('REDIS_CLUSTER', 'redis'), | ||||
|             'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), | ||||
|         ], | ||||
| 
 | ||||
|         'default' => [ | ||||
|             'url' => env('REDIS_URL'), | ||||
|             'host' => env('REDIS_HOST', '127.0.0.1'), | ||||
|             'username' => env('REDIS_USERNAME'), | ||||
|             'password' => env('REDIS_PASSWORD'), | ||||
|             'port' => env('REDIS_PORT', '6379'), | ||||
|             'database' => env('REDIS_DB', '0'), | ||||
|         ], | ||||
| 
 | ||||
|         'cache' => [ | ||||
|             'url' => env('REDIS_URL'), | ||||
|             'host' => env('REDIS_HOST', '127.0.0.1'), | ||||
|             'username' => env('REDIS_USERNAME'), | ||||
|             'password' => env('REDIS_PASSWORD'), | ||||
|             'port' => env('REDIS_PORT', '6379'), | ||||
|             'database' => env('REDIS_CACHE_DB', '1'), | ||||
|         ], | ||||
| 
 | ||||
|     ], | ||||
| 
 | ||||
| ]; | ||||
|  |  | |||
|  | @ -115,6 +115,10 @@ div.preview pre.small, div.project pre.small { | |||
| div.preview pre, div.project pre { | ||||
|     background-color: #222; | ||||
|     color: #ccc; | ||||
|     display: inline-block; | ||||
|     text-align: left; | ||||
|     padding: 0.2em; | ||||
|     max-width: 90% | ||||
| } | ||||
| 
 | ||||
| div.project pre { | ||||
|  | @ -283,13 +287,6 @@ .header { | |||
|     color: #FFFFFF; | ||||
| } | ||||
| 
 | ||||
| div.preview pre, div.project pre { | ||||
|     display: inline-block; | ||||
|     text-align: left; | ||||
|     padding: 0.2em; | ||||
|     max-width: 90% | ||||
| } | ||||
| 
 | ||||
| h1 { font-size: 150% } | ||||
| h2 { font-size: 130% } | ||||
| h3 { font-size: 115% } | ||||
|  | @ -324,3 +321,32 @@ a { | |||
|     color: #99f; | ||||
|     text-decoration: none | ||||
| } | ||||
| 
 | ||||
| table.gb_entryform tr td { | ||||
|     border: none; | ||||
| } | ||||
| 
 | ||||
| table.gb_entryform tr td label { | ||||
|     padding-right: 5px; | ||||
| } | ||||
| 
 | ||||
| table.gb_entryform tr td span.text-danger { | ||||
|     padding-left: 5px; | ||||
|     color: rgb(255, 114, 114); | ||||
| } | ||||
| 
 | ||||
| table.gb_entryform tr td textarea, | ||||
| table.gb_entryform tr td input { | ||||
|     margin-bottom: 5px; | ||||
| } | ||||
| 
 | ||||
| table.gb_entry tr td { | ||||
|     border: solid #FFFFFF; | ||||
|     width: 500px; | ||||
|     vertical-align: top; | ||||
|     padding: 5px; | ||||
| } | ||||
| 
 | ||||
| table.gb_entry { | ||||
|     margin-bottom: 5px; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										11
									
								
								public/css/minimal.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								public/css/minimal.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| html { | ||||
|     color-scheme: dark; | ||||
| } | ||||
| 
 | ||||
| body { | ||||
|     font-family: sans-serif; | ||||
|     margin: 0px; | ||||
|     margin-left: 10px; | ||||
|     color: #ddd; | ||||
|     background-color: #333; | ||||
| } | ||||
							
								
								
									
										16
									
								
								resources/views/errors/ratelimit-guestbook.blade.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								resources/views/errors/ratelimit-guestbook.blade.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | |||
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <title>Error 429: Overclocking Detected!</title> | ||||
|     <link rel="stylesheet" href="{{ URL::asset ('css/minimal.css') }}"/> | ||||
| </head> | ||||
| 
 | ||||
| <body> | ||||
|     <h1>Error 429: Overclocking Detected!</h1> | ||||
|     <hr/> | ||||
|     <p>Whoa there! Your submissions are going at warp speed.</p> | ||||
|     <p>Remember you can only submit an entry <u>once every hour</u>!</p> | ||||
|     <br/> | ||||
|     Click <a href="/guestbook">here</a> to go back to the guestbook. | ||||
| </body> | ||||
| </html> | ||||
|  | @ -6,6 +6,7 @@ | |||
|             <a href="/projects/">projects</a> | | ||||
|             <a href="/calculators/">calculators</a> | | ||||
|             <a href="/computers/">computers</a> | | ||||
|             <a href="/bookmarks/">bookmarks</a> | ||||
|             <a href="/bookmarks/">bookmarks</a> | | ||||
|             <a href="/guestbook/">guestbook</a> | ||||
|         </div> | ||||
|     </nav> | ||||
|  |  | |||
							
								
								
									
										53
									
								
								resources/views/pages/guestbook.blade.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								resources/views/pages/guestbook.blade.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | |||
| @extends('layouts.default') | ||||
| @section('title', 'guestbook') | ||||
| @section('content') | ||||
|     <br/> | ||||
|     <form method="POST" action="/guestbook"> | ||||
|         @csrf | ||||
|         <x-honeypot /> | ||||
|         <table class="gb_entryform"> | ||||
|             <tr> | ||||
|                 <td> | ||||
|                     <label for="name">Name:</label> | ||||
|                 </td> | ||||
|                 <td> | ||||
|                     <input name="name" type="text" id="name" placeholder="John Doe"> | ||||
|                 </td> | ||||
|                 <td> | ||||
|                     <span class="text-danger">{{ $errors->first('name') }}</span> | ||||
|                 </td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td> | ||||
|                     <label for="message">Message:</label> | ||||
|                 </td> | ||||
|                 <td> | ||||
|                     <textarea name="message" id="message" rows="3"></textarea> | ||||
|                 </td> | ||||
|                 <td> | ||||
|                     <span class="text-danger">{{ $errors->first('message') }}</span> | ||||
|                 </td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td colspan="2"> | ||||
|                     <button type="submit">Submit</button> | ||||
|                 </td> | ||||
|             </tr> | ||||
|         </table> | ||||
|     </form> | ||||
|     <p>You can submit an entry <u>once every hour</u>.</p> | ||||
|     <p>Your IP address <u>will</u> be logged but <u>will not</u> be publically displayed.</p> | ||||
|     <hr/> | ||||
|     @php | ||||
|         $entries = DB::select('SELECT name, timestamp, message FROM guestbook_entries ORDER BY id DESC'); | ||||
|     @endphp | ||||
|     <h1>Entries <small>({{ count($entries) }} total)</small></h1> | ||||
|     @foreach ($entries as $entry) | ||||
|         <table class="gb_entry"><tr><td> | ||||
|             Name: {{ $entry->name }}<br/> | ||||
|             Date: {{ gmdate("H:i:s - Y-m-d", $entry->timestamp) }}<br/><br/> | ||||
|             {{ htmlspecialchars($entry->message) }} | ||||
|         </td></tr></table> | ||||
|     @endforeach | ||||
| @stop | ||||
| 
 | ||||
|  | @ -32,3 +32,12 @@ | |||
| Route::get('/computers', function () { | ||||
|     return View::make('pages.computers'); | ||||
| }); | ||||
| 
 | ||||
| Route::get('/guestbook', 'App\Http\Controllers\GuestbookController@guestbook') | ||||
|     ->name('guestbook'); | ||||
| 
 | ||||
| Route::post('/guestbook', 'App\Http\Controllers\GuestbookController@guestbookpost') | ||||
|     ->name('guestbookPost') | ||||
|     ->middleware('rate_limit'); | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Frankie B
				Frankie B