Compare commits

..

2 commits

Author SHA1 Message Date
af0907c7c3
There be a website 2025-09-21 03:09:52 +01:00
64fb080e96
Initialize rails app 2025-09-20 03:31:27 +01:00
243 changed files with 1693 additions and 14140 deletions

View file

@ -1,28 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
LASTFM_KEY=
LASTFM_USER=

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

16
.gitattributes vendored
View file

@ -1,11 +1,9 @@
* text=auto eol=lf
# See https://git-scm.com/docs/gitattributes for more about git attribute files.
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
# Mark the database schema as having been generated.
db/schema.rb linguist-generated
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore
# Mark any vendored files as having been vendored.
vendor/* linguist-vendored
config/credentials/*.yml.enc diff=rails_credentials
config/credentials.yml.enc diff=rails_credentials

1
.github/CODEOWNERS vendored
View file

@ -1 +0,0 @@
* @RoscoeDaWah

6
.gitignore vendored
View file

@ -22,3 +22,9 @@ yarn-error.log
/storage
/tmp
/public/pub
.old
# Ignore master key for decrypting credentials and more.
/config/master.key
/config/credentials.yml.enc

1
.ruby-version Normal file
View file

@ -0,0 +1 @@
ruby-3.4.1

View file

@ -1,5 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Development Server (External)" type="PhpLocalRunConfigurationType" factoryName="PHP Console" path="$PROJECT_DIR$/artisan" scriptParameters="serve --host 0.0.0.0 --port 8000">
<method v="2" />
</configuration>
</component>

View file

@ -1,5 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Development Server" type="PhpLocalRunConfigurationType" factoryName="PHP Console" path="$PROJECT_DIR$/artisan" scriptParameters="serve">
<method v="2" />
</configuration>
</component>

20
Gemfile Normal file
View file

@ -0,0 +1,20 @@
source "https://rubygems.org"
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 8.0.2", ">= 8.0.2.1"
# The modern asset pipeline for Rails [https://github.com/rails/propshaft]
gem "propshaft"
# Use sqlite3 as the database for Active Record
gem "sqlite3", ">= 2.1"
# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ windows jruby ]
gem "view_component"
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
end

239
Gemfile.lock Normal file
View file

@ -0,0 +1,239 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.2.1)
actionpack (= 8.0.2.1)
activejob (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
mail (>= 2.8.0)
actionmailer (8.0.2.1)
actionpack (= 8.0.2.1)
actionview (= 8.0.2.1)
activejob (= 8.0.2.1)
activesupport (= 8.0.2.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.2.1)
actionview (= 8.0.2.1)
activesupport (= 8.0.2.1)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.2.1)
actionpack (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.2.1)
activesupport (= 8.0.2.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.0.2.1)
activesupport (= 8.0.2.1)
globalid (>= 0.3.6)
activemodel (8.0.2.1)
activesupport (= 8.0.2.1)
activerecord (8.0.2.1)
activemodel (= 8.0.2.1)
activesupport (= 8.0.2.1)
timeout (>= 0.4.0)
activestorage (8.0.2.1)
actionpack (= 8.0.2.1)
activejob (= 8.0.2.1)
activerecord (= 8.0.2.1)
activesupport (= 8.0.2.1)
marcel (~> 1.0)
activesupport (8.0.2.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
base64 (0.3.0)
benchmark (0.4.1)
bigdecimal (3.2.3)
builder (3.3.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.4)
crass (1.0.6)
date (3.4.1)
debug (1.11.0)
irb (~> 1.10)
reline (>= 0.3.8)
drb (2.2.3)
erb (5.0.2)
erubi (1.13.1)
globalid (1.2.1)
activesupport (>= 6.1)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
io-console (0.8.1)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
logger (1.7.0)
loofah (2.24.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.1.0)
mini_mime (1.1.5)
minitest (5.25.5)
net-imap (0.5.10)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.10-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-musl)
racc (~> 1.4)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
propshaft (1.2.1)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
psych (5.2.6)
date
stringio
puma (7.0.3)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.2.1)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
rack (>= 1.3)
rackup (2.2.1)
rack (>= 3)
rails (8.0.2.1)
actioncable (= 8.0.2.1)
actionmailbox (= 8.0.2.1)
actionmailer (= 8.0.2.1)
actionpack (= 8.0.2.1)
actiontext (= 8.0.2.1)
actionview (= 8.0.2.1)
activejob (= 8.0.2.1)
activemodel (= 8.0.2.1)
activerecord (= 8.0.2.1)
activestorage (= 8.0.2.1)
activesupport (= 8.0.2.1)
bundler (>= 1.15.0)
railties (= 8.0.2.1)
rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
railties (8.0.2.1)
actionpack (= 8.0.2.1)
activesupport (= 8.0.2.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rake (13.3.0)
rdoc (6.14.2)
erb
psych (>= 4.0.0)
reline (0.6.2)
io-console (~> 0.5)
securerandom (0.4.1)
sqlite3 (2.7.4-aarch64-linux-gnu)
sqlite3 (2.7.4-aarch64-linux-musl)
sqlite3 (2.7.4-arm-linux-gnu)
sqlite3 (2.7.4-arm-linux-musl)
sqlite3 (2.7.4-arm64-darwin)
sqlite3 (2.7.4-x86_64-darwin)
sqlite3 (2.7.4-x86_64-linux-gnu)
sqlite3 (2.7.4-x86_64-linux-musl)
stringio (3.1.7)
thor (1.4.0)
timeout (0.4.3)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uri (1.0.3)
useragent (0.16.11)
view_component (4.0.2)
activesupport (>= 7.1.0, < 8.1)
concurrent-ruby (~> 1)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.7.3)
PLATFORMS
aarch64-linux
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
x86_64-darwin
x86_64-linux
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
debug
propshaft
puma (>= 5.0)
rails (~> 8.0.2, >= 8.0.2.1)
sqlite3 (>= 2.1)
tzinfo-data
view_component
BUNDLED WITH
2.7.1

View file

@ -1,3 +1,3 @@
<img src="https://git.frzn.dev/RoscoeDaWah/wah.moe/raw/branch/master/assets/logo.svg" alt="" height="100" align="center"/>
<img src="https://git.frzn.dev/RoscoeDaWah/wah.moe/raw/branch/master/.forgejo/assets/logo.svg" alt="" height="100" align="center"/>
<hr>
My personal website, developed using the Laravel framework
My personal website, developed using the Rails framework

6
Rakefile Normal file
View file

@ -0,0 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative "config/application"
Rails.application.load_tasks

View file

@ -1,27 +0,0 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
}
/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

View file

@ -1,32 +0,0 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
}
}

View file

@ -1,15 +0,0 @@
<?php
if (!function_exists('isLegacy')) {
/**
* Checks if the current hostname should return the legacy site
* @return bool
*/
function isLegacy(): bool {
return (
request()->getHost() === "legacy.wah.moe" || // Accessed via legacy.wah.moe domain
str_starts_with(request()->getHost(), "192.168") || // Accessed via local IP address
!request()->hasHeader("Host") // Browser does not send Host header (e.g. NCSA MOSAIC)
);
}
}

View file

@ -1,15 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\BookmarkSite;
use App\Models\BookmarkCategory;
use Illuminate\View\View;
class BookmarksController extends Controller
{
public function show() : View {
$categories = BookmarkCategory::with('sites')->get();
return view('bookmarks', compact('categories'));
}
}

View file

@ -1,12 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
}

View file

@ -1,33 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\GuestbookEntry;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\Contracts\View\View;
use Illuminate\Validation\ValidationException;
use UAParser\Parser;
class GuestbookController extends Controller {
public function show(): View {
$entries = GuestbookEntry::selectEntries();
$parser = Parser::create();
return view('guestbook')
->with('entries', $entries)
->with('parser', $parser);
}
/**
* Creates a new guestbook entry
*
* @param Request $request
* @return RedirectResponse
* @throws ValidationException
*/
public function addEntry(Request $request): RedirectResponse {
GuestbookEntry::insertGuestbookEntry($request);
return back()->with('success', 'Entry submitted successfully!');
}
}

View file

@ -1,33 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Http;
use Illuminate\View\View;
use DateTime;
class HomeController extends Controller {
/**
* Returns age based on birthday date and current date (GMT)
* @return int
*/
function returnAge(): int {
date_default_timezone_set('Europe/London');
$birthday = new DateTime("2005-06-07");
$currentDate = DateTime::createFromFormat("Y-m-d", date("Y-m-d"));
$age = $birthday->diff($currentDate);
return $age->y;
}
/**
* Shows home page
* @return View
*/
public function show(): View {
return view('home', [
'age' => $this->returnAge(),
]);
}
}

View file

@ -1,76 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Http;
use Illuminate\View\View;
class MusicController extends Controller
{
public function getCurrentTrack() {
// If it's already cached just return that
if (Cache::has('current_track')) {
return Cache::get('current_track');
}
$response = Http::withQueryParameters([
'method' => 'user.getrecenttracks',
'user' => Config::get('services.lastfm.user'),
'format' => 'json',
'nowplaying' => 'true',
'api_key' => Config::get('services.lastfm.key')
])->get('https://ws.audioscrobbler.com/2.0/');
$data = $response->json();
$track_data = $data["recenttracks"]["track"][0];
// $image = array_column($track_data["image"], null, 'size')['large'] ?? false;
$image = end($track_data["image"]) ?? false;
$now_playing = false;
if (array_key_exists("@attr", $track_data)) {
$now_playing = $track_data["@attr"]["nowplaying"] == "true" ?? ["url"=>null];
}
$current_track = [
'title' => $track_data["name"],
'artist' => $track_data["artist"]["#text"],
'url' => $track_data["url"],
'image' => $image["#text"],
'header' => $now_playing ? "Now Playing" : "Last Track",
];
Cache::put('current_track', $current_track, now()->addSeconds(15));
return $current_track;
}
public function getTopTracks() {
// If it's already cached just return that
if (Cache::has('top_tracks')) {
return Cache::get('top_tracks');
}
$response = Http::withQueryParameters([
'method' => 'user.gettoptracks',
'user' => Config::get('services.lastfm.user'),
'format' => 'json',
'period' => '1month',
'limit' => 10,
'api_key' => Config::get('services.lastfm.key')
])->get('https://ws.audioscrobbler.com/2.0/');
$data = $response->json();
$topTracks = [];
foreach ($data["toptracks"]["track"] as $track) {
$topTracks[] = [
'title' => $track["name"],
'artist' => $track["artist"]["name"],
'url' => $track["url"],
'plays' => $track["playcount"],
];
}
Cache::put('top_tracks', $topTracks, now()->addSeconds(15));
return $topTracks;
}
public function show() : View {
return view('music')
->with('current_track', $this->getCurrentTrack())
->with('top_tracks', $this->getTopTracks());
}
}

View file

@ -1,52 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\File;
use Illuminate\View\View;
class RoscoLekoController extends Controller {
public function getImages(): array {
$images = [];
$path = 'images/pandamonium';
if (isLegacy()) {
$path = 'images/pandamonium-legacy';
}
foreach (File::glob(public_path($path).'/*') as $path) {
$image_data = [];
try {
$exif = exif_read_data($path);
} catch (Exception $ex) {
}
$image_data["path"] = str_replace(public_path(), '', $path);
if (isset($exif)) {
if (isset($exif["ImageDescription"])) {
$image_data["description"] = $exif["ImageDescription"];
}
if (isset($exif["DateTime"])) {
$image_data["date"] = strtotime($exif["DateTime"]);
}
}
array_push($images, $image_data);
}
usort($images, function ($a, $b) {
$dateA = $a['date'] ?? PHP_INT_MIN;
$dateB = $b['date'] ?? PHP_INT_MIN;
return $dateB <=> $dateA;
});
return $images;
}
/**
* Shows the page
* @return View
*/
public function show(): View {
return view('pandamonium', [
'images' => $this->getImages(),
]);
}
}

View file

@ -1,73 +0,0 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
protected $routeMiddleware = [
'rate_limit' => \App\Http\Middleware\RateLimiter::class,
'validator' => \App\Http\Middleware\GuestbookValidate::class,
];
/**
* The application's middleware aliases.
*
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}

View file

@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
}

View file

@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
"colorscheme"
];
}

View file

@ -1,37 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class GuestbookValidate
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (
!$request->validate([
'name' => 'required',
'message' => 'required'
]) ||
$this->containsUrl($request->get('message')) ||
$this->containsUrl($request->get('name'))
) {
return response()->view('errors.guestbook-invalid', [], 400);
}
return $next($request);
}
public function containsUrl($str) {
$matches = [];
$pattern = '/\b(?:https?|ftp|www)(:\/\/)*[-A-Z0-9+&@#\/%?=~_|$!:,.;]*[A-Z0-9+&@#\/%=~_|$]/i';
preg_match_all($pattern, $str, $matches);
return count($matches[0]);
}
}

View file

@ -1,15 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [];
}

View file

@ -1,35 +0,0 @@
<?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
{
if (auth()->check()) {
return $next($request);
}
$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.guestbook-ratelimit', [], 429);
}
// Add the IP address to the cache and set the expiration time to one hour.
Cache::put($cacheKey, true, 3600);
return $next($request);
}
}

View file

@ -1,30 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
} // End handle().
}

View file

@ -1,19 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts(): array
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View file

@ -1,29 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =(
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB
);
}

View file

@ -1,22 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}

View file

@ -1,15 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [];
}

View file

@ -1,36 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class BookmarkCategory extends Model
{
use HasFactory;
protected $table = "bookmark__categories";
protected $fillable = ['name'];
public function sites() {
return $this->hasMany(BookmarkSite::class, 'category');
}
public static function insertBookmarkCategory(string $name) {
$newBookmarkCategory = new BookmarkCategory;
$newBookmarkCategory->name = $name;
$newBookmarkCategory->save();
}
public static function selectBookmarks(int $id) {
$bookmarks = BookmarkSite::where('category', '=', $id)->firstOrFail();
return $bookmarks;
}
public static function importBookmarkCategory(array $data) {
foreach ($data as $category) {
$newBookmarkCategory = new BookmarkCategory;
$newBookmarkCategory->name = $category['name'];
$newBookmarkCategory->priority = intval($category['priority']);
$newBookmarkCategory->save();
}
}
}

View file

@ -1,35 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class BookmarkSite extends Model {
use HasFactory;
protected $table = "bookmark__sites";
protected $fillable = ['name', 'description', 'url', 'category'];
public function category() {
return $this->belongsTo(BookmarkCategory::class, 'category');
}
public static function insertBookmark(string $name, string $url, int $category) {
$category = BookmarkCategory::where('id', $category)->firstOrFail();
$newBookmark = new BookmarkSite;
$newBookmark->name = $name;
$newBookmark->url = $url;
$newBookmark->category = $category->id;
$newBookmark->save();
}
public static function importBookmark(array $data) {
foreach ($data as $site) {
$newBookmark = new BookmarkSite;
$newBookmark->name = $site['name'];
$newBookmark->description = $site['description'];
$newBookmark->url = $site['url'];
$newBookmark->category = $site['category_id'];
$newBookmark->save();
}
}
}

View file

@ -1,50 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class GuestbookEntry extends Model
{
use HasFactory;
protected $table = "guestbook__entries";
protected $fillable = ['name', 'message'];
/**
* Creates a new guestbook entry.
*
* @param Request $request The HTTP POST request
* @return void
*/
public static function insertGuestbookEntry(Request $request) {
$newEntry = new GuestbookEntry;
$newEntry->name = $request->get('name');
$newEntry->message = $request->get('message');
$newEntry->ip = $request->ip();
$newEntry->agent = $request->userAgent();
$newEntry->legacy_flagged = isLegacy();
$newEntry->save();
}
public static function selectEntries() {
$entries = GuestbookEntry::where("legacy_flagged", false)->orderBy('created_at', 'desc')->get();
return $entries;
}
public static function importGuestbookEntry(array $data) {
foreach ($data as $entry) {
$dt = new \DateTime('@' . $entry['timestamp']);
$newEntry = new GuestbookEntry;
$newEntry->name = $entry['name'];
$newEntry->ip = $entry['ip_address'];
$newEntry->agent = $entry['agent'];
$newEntry->admin = $entry['site_owner'];
$newEntry->message = $entry['message'];
$newEntry->created_at = $dt;
$newEntry->updated_at = $dt;
$newEntry->save();
}
}
}

View file

@ -1,21 +0,0 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\ServiceProvider;
use PostHog\PostHog;
class AppServiceProvider extends ServiceProvider {
/**
* Register any application services.
*/
public function register(): void {
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void {}
}

View file

@ -1,25 +0,0 @@
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [
//
];
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
//
}
}

View file

@ -1,19 +0,0 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Broadcast::routes();
require base_path('routes/channels.php');
}
}

View file

@ -1,38 +0,0 @@
<?php
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
/**
* Register any events for your application.
*/
public function boot(): void
{
//
}
/**
* Determine if events and listeners should be automatically discovered.
*/
public function shouldDiscoverEvents(): bool
{
return false;
}
}

View file

@ -1,40 +0,0 @@
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to your application's "home" route.
*
* Typically, users are redirected here after authentication.
*
* @var string
*/
public const HOME = '/home';
/**
* Define your route model bindings, pattern filters, and other route configuration.
*/
public function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
}

View file

@ -1,24 +0,0 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class LastFMCurrent extends Component {
public $track;
/**
* Create a new component instance.
*/
public function __construct($track) {
$this->track = $track;
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string {
return view('components.lastfm-current');
}
}

View file

@ -1,24 +0,0 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class LastFMTop extends Component {
public $tracks;
/**
* Create a new component instance.
*/
public function __construct($tracks) {
$this->tracks = $tracks;
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string {
return view('components.lastfm-top');
}
}

View file

@ -1,26 +0,0 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class LastFMTrack extends Component {
public $track;
public $count;
/**
* Create a new component instance.
*/
public function __construct($track, $count) {
$this->track = $track;
$this->count = $count;
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string {
return view('components.lastfm-track');
}
}

View file

@ -1,40 +0,0 @@
<?php
namespace App\View\Components;
use Closure;
use DateTime;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Layout extends Component {
/**
* Create a new component instance.
*/
public function __construct() {}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string {
if (isLegacy()) {
return view('components.layout-legacy', [
'isChristmas' => $this->isItChristmas()
]);
} else {
return view('components.layout', [
'isChristmas' => $this->isItChristmas()
]);
}
}
public function isItChristmas() : bool {
$currentDate = new DateTime();
$currentYear = intval($currentDate->format('Y'));
$startDate = new DateTime("$currentYear-11-10");
$endDate = new DateTime(($currentYear + 1) . "-01-01");
return $currentDate >= $startDate && $currentDate < $endDate;
}
}

View file

@ -1,24 +0,0 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Navbar extends Component {
public $title;
/**
* Create a new component instance.
*/
public function __construct($title) {
$this->title = $title;
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string {
return view('components.navigation');
}
}

View file

@ -1,50 +0,0 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Http;
use Illuminate\View\Component;
class Wah extends Component {
/**
* Create a new component instance.
*/
public function __construct() {}
public function getWah(): string {
if (isLegacy()) {
try {
$response = Http::get('https://api.tinyfox.dev/img.json?animal=wah');
$data = $response->json();
if ($data == null) return "";
$path = parse_url("https://api.tinyfox.dev" . $data["loc"], PHP_URL_PATH);
return "//".request()->getHttpHost()."/proxy/wah/".basename($path);
} catch (Exception $ex) {
return "";
}
}
try {
$response = Http::get('https://api.tinyfox.dev/img.json?animal=wah');
$data = $response->json();
if ($data == null) return "";
return "https://api.tinyfox.dev" . $data["loc"];
} catch (Exception $ex) {
return "";
}
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string {
return view('components.wah', [
'wah' => $this->getWah(),
]);
}
}

0
app/assets/images/.keep Normal file
View file

View file

Before

Width:  |  Height:  |  Size: 837 KiB

After

Width:  |  Height:  |  Size: 837 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 786 B

After

Width:  |  Height:  |  Size: 786 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 524 B

After

Width:  |  Height:  |  Size: 524 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 495 B

After

Width:  |  Height:  |  Size: 495 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 478 B

After

Width:  |  Height:  |  Size: 478 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 891 B

After

Width:  |  Height:  |  Size: 891 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 996 B

After

Width:  |  Height:  |  Size: 996 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 471 B

After

Width:  |  Height:  |  Size: 471 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 899 B

After

Width:  |  Height:  |  Size: 899 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 764 B

After

Width:  |  Height:  |  Size: 764 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 746 B

After

Width:  |  Height:  |  Size: 746 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 833 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 538 KiB

After

Width:  |  Height:  |  Size: 538 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 587 KiB

After

Width:  |  Height:  |  Size: 587 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 746 KiB

After

Width:  |  Height:  |  Size: 746 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.6 MiB

After

Width:  |  Height:  |  Size: 6.6 MiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 422 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2 MiB

After

Width:  |  Height:  |  Size: 2 MiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 571 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Before After
Before After

View file

@ -1,3 +1,14 @@
/*
* This is a manifest file that'll be compiled into application.css.
*
* With Propshaft, assets are served efficiently without preprocessing steps. You can still include
* application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
* cascading order, meaning styles declared later in the document or manifest will override earlier ones,
* depending on specificity.
*
* Consider organizing styles into separate files for maintainability.
*/
:root {
--background: hsl(214, 67%, 85%);
--foreground: hsl(214, 20%, 14%);
@ -14,56 +25,56 @@ :root {
/* ───────────────────────────────────── Fonts ────────────────────────────────────── */
@font-face {
font-family: "PT Sans";
src: url("/fonts/PTSans-Regular.ttf") format("truetype");
src: url("PTSans-Regular.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "PT Sans";
src: url("/fonts/PTSans-Italic.ttf") format("truetype");
src: url("PTSans-Italic.ttf") format("truetype");
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: "PT Sans";
src: url("/fonts/PTSans-Bold.ttf") format("truetype");
src: url("PTSans-Bold.ttf") format("truetype");
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: "PT Sans";
src: url("/fonts/PTSans-BoldItalic.ttf") format("truetype");
src: url("PTSans-BoldItalic.ttf") format("truetype");
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: "PT Serif";
src: url("/fonts/PTSerif-Regular.ttf") format("truetype");
src: url("PTSerif-Regular.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "PT Serif";
src: url("/fonts/PTSerif-Italic.ttf") format("truetype");
src: url("PTSerif-Italic.ttf") format("truetype");
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: "PT Serif";
src: url("/fonts/PTSerif-Bold.ttf") format("truetype");
src: url("PTSerif-Bold.ttf") format("truetype");
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: "PT Serif";
src: url("/fonts/PTSerif-BoldItalic.ttf") format("truetype");
src: url("PTSerif-BoldItalic.ttf") format("truetype");
font-weight: bold;
font-style: italic;
}
@ -98,7 +109,7 @@ html {
body {
color: var(--foreground);
min-height: 100%;
background: url('/images/roscoe_tile.jpg');
background: url('roscoe_tile.jpg');
padding: 5px;
font-family: "PT Serif", serif;
}

View file

@ -0,0 +1,5 @@
<div class="wah">
<h3>Random Wah!</h3>
<%= image_tag @wah_url, alt: "Random image featuring a red panda" %>
<p>Image "stolen" from <a href="https://tinyfox.dev/">tinyfox.dev</a></p>
</div>

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
require "net/http"
require "json"
class WahComponent < ViewComponent::Base
def initialize(legacy:)
if legacy then
else
uri = URI.parse('https://api.tinyfox.dev/img.json?animal=wah')
req = Net::HTTP::Get.new(uri.to_s)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) {|http|
http.request(req)
}
data = JSON.parse(res.body)
url = "https://tinyfox.dev" + data["loc"]
@wah_url = url
end
end
end

View file

@ -0,0 +1,4 @@
class ApplicationController < ActionController::Base
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
allow_browser versions: :modern
end

View file

View file

@ -0,0 +1,9 @@
class HomeController < ApplicationController
def years_between_dates(date_from, date_to)
return (date_to - date_from).to_i / 365
end
def index
@age = years_between_dates(DateTime.civil_from_format(:local, 2005, 6, 7), DateTime.now)
end
end

Some files were not shown because too many files have changed in this diff Show more