#!/usr/bin/perl -w use lib qw(public); use Mojolicious::Lite -signatures; use Path::Iterator::Rule; use WWWDBI; use Tools; use Mojo::Util qw(trim); use HTML::Entities qw(encode_entities); use Cwd; use IO::File; use URI; my $db = WWWDBI->new(); my $secret = $db->getAppSecret(); app->secrets([$secret]); app->sessions->cookie_name('session'); app->config( hypnotoad => { listen => [ 'http://127.0.0.1:8082/' ], proxy => 1, }, ); helper listFiles => sub { my @locations = ('public', 'templates'); my @all_files; my $rule = Path::Iterator::Rule->new->not_dir->name(qr/(pm|pl|js|css|ep)$/); for my $location (@locations) { push @all_files, $rule->all($location); } (my $file = __FILE__) =~ s{.*/}{}; push @all_files, $file; my @sorted = sort { fc($a) cmp fc($b) } @all_files; return @sorted; }; helper is_logged_in => sub ($c) { return $c->session('user') ? 1 : 0; }; helper is_admin => sub ($c) { return 0 unless $c->session('user'); return $db->is_admin($c->session('user')); }; helper is_allowed_ip => sub ($c, $ip) { my $allowed_range = '^10\.0\.0\.\d+$'; return $ip =~ /$allowed_range/; }; helper render_error => sub ($c, $message, $status = 400) { $c->app->log->error($message); return $c->render(json => { error => $message }, status => $status); }; helper get_domain => sub ($c, $url) { my $uri = URI->new($url); return $uri->host; }; helper parse_swin_data => sub ($c, $file_path) { my @categories; my $current_category = -1; my $fh = IO::File->new($file_path, 'r') or do { $c->app->log->error("Could not open bookmark file: $file_path - $!"); return undef; }; while (my $line = <$fh>) { chomp $line; next if $line =~ /^\s*$/; if ($line =~ s/^>>\s*// && $current_category >= 0) { my ($name, $url) = split /\s*\|\s*/, $line, 2; if (defined $name && defined $url) { my $domain = $c->get_domain($url); push @{ $categories[$current_category]->{links} }, { name => $name, url => $url, domain => $domain }; } } elsif ($line =~ s/^>\s*//) { push @categories, { title => $line, links => [] }; $current_category++; } } $fh->close; return @categories; }; helper save_swin_data => sub ($c, $categories_ref) { my $file_path = 'swin'; my $fh = IO::File->new($file_path, 'w') or do { $c->app->log->error("Could not write to bookmark file: $file_path - $!"); return 0; }; foreach my $category (@$categories_ref) { $fh->print(">" . $category->{title} . "\n"); foreach my $link (@{ $category->{links} }) { $fh->print(">>" . $link->{name} . " | " . $link->{url} . "\n"); } $fh->print("\n"); } $fh->close; return 1; }; get '/cwd' => sub ($c) { $c->render(text => "CWD: " . Cwd::getcwd()); }; get '/' => sub ($c) { my $client_ip = $c->tx->remote_address; my $last_visit = $c->session('last_visit') || time(); $c->session(last_visit => time()); $c->stash( client_ip => $client_ip, last_visit => $last_visit, ); $c->stash(is_logged_in => $c->is_logged_in); $c->render(template => 'index'); }; get '/protected' => sub ($c) { return $c->redirect_to('/login') unless $c->is_logged_in; $c->render(text => 'This is a protected page'); }; get '/source' => sub ($c) { if ($c->param('f') =~ m{^(public|templates)/} || $c->param('f') eq (__FILE__ =~ s{.*/}{}r)) { my $text = source($c->param('f')); $c->render(text => $text, format => 'txt'); } else { $c->render(text => "Access denied.", format => 'txt', status => 403); } }; get '/age' => sub ($c) { my $dob = WWWDBI->new()->dob(); my @andrea = howOld($dob->{andrea}->{dob}); my @nicky = howOld($dob->{nicky}->{dob}); $c->render( json => { andrea => $andrea[0], andreas => $andrea[1], nicky => $nicky[0], nickys => $nicky[1], }); }; get '/t' => sub ($c) { $c->render(template => 't') }; my $rNum = sub ($c) { $c->render(template => 'p') }; get '/m' => $rNum; get '/p' => $rNum; get '/phone' => $rNum; get '/mobile' => $rNum; get '/copy' => sub ($c) { my @msgs = WWWDBI->new()->getPasted(); my $client_ip = $c->tx->remote_address; my $is_allowed = $c->is_allowed_ip($client_ip); $c->stash(messages => \@msgs, client_ip => $client_ip, is_allowed => $is_allowed); $c->render(template => 'copy'); }; post '/copy' => sub ($c) { my $text = trim($c->param('paste') // ''); return $c->render_error('Input too long') if length($text) > 250; $text = encode_entities($text); if ($text =~ m{^https?://}i) { unless ($text =~ m{^https?://[\w\-]+(?:\.[\w\-]+)+(?:/[^\s]*)?$}i) { return $c->render_error('Invalid URL'); } } $db->paste($text); $db->pushOver($text); my @msgs = $db->getPasted(); my $client_ip = $c->tx->remote_address; my $is_allowed = $c->is_allowed_ip($client_ip); $c->stash(messages => \@msgs, client_ip => $client_ip, is_allowed => $is_allowed); $c->render(template => 'copy'); }; post '/delete' => sub ($c) { unless ($c->is_logged_in && $c->is_allowed_ip($c->tx->remote_address)) { return $c->render_error('Unauthorized', 403); } my $id = $c->param('id'); unless (defined $id && $id =~ /^\d+$/) { return $c->render_error('Invalid ID'); } $db->deleteMessage($id); $c->redirect_to('/copy'); }; get 'this.is.totally.not.sus' => sub ($c) { $c->render(template => 'sus'); }; sub contact_page ($c) { my @qr_images = qw(discord.png email.png line.jpg messenger.png); $c->render(template => 'contact', qr_images => \@qr_images); } get '/contacts' => \&contact_page; get '/contact' => \&contact_page; get '/c' => \&contact_page; get '/login' => sub ($c) { $c->render(template => 'login'); }; post '/login' => sub ($c) { my $username = trim($c->param('username') // ''); my $password = $c->param('password'); if ($db->authenticateUser($username, $password)) { $c->session(user => $username); $c->app->log->info("User $username logged in from IP " . $c->tx->remote_address); return $c->redirect_to('/'); } else { $c->app->log->warn("Failed login attempt for user $username from IP " . $c->tx->remote_address); return $c->render_error('Login failed'); } }; get '/logout' => sub ($c) { $c->session(expires => 1); return $c->redirect_to('/'); }; get '/register' => sub ($c) { $c->render(template => 'register'); }; post '/register' => sub ($c) { my $username = trim($c->param('username') // ''); my $password = $c->param('password'); my $email = trim($c->param('email') // ''); return $c->render_error('Invalid username') unless $username =~ /^[a-zA-Z0-9_]{3,20}$/; return $c->render_error('Password too short') if length($password) < 8; return $c->render_error('Invalid email') unless $email =~ /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; if ($db->userExists($username)) { return $c->render_error('Username already exists'); } eval { $db->createUser($username, $password, $email); }; if (my $error = $@) { $c->app->log->error("Failed to create user: $error"); return $c->render_error("Error creating user: $error", 500); } $c->session(user => $username); $c->app->log->info("New user registered: $username from IP " . $c->tx->remote_address); return $c->redirect_to('/'); }; get '/users' => sub ($c) { unless ($c->session('user')) { return $c->redirect_to('/login'); } my $username = $c->session('user'); unless ($db->is_admin($username)) { return $c->render(text => 'Access denied', status => 403); } my $users = $db->get_all_users(); $c->stash(users => $users); $c->render(template => 'users_admin'); }; get '/swin' => sub ($c) { my $file_path = 'swin'; my @categories = $c->parse_swin_data($file_path); return $c->render(text => 'Error: Could not load bookmark data.', status => 500) unless @categories; $c->stash(categories => \@categories); $c->render(template => 'swin'); }; get '/swin-edit' => sub ($c) { return $c->render_error('Access denied', 403) unless $c->is_admin; my $file_path = 'swin'; my @categories = $c->parse_swin_data($file_path); return $c->render(text => 'Error: Could not load bookmark data.', status => 500) unless @categories; $c->stash(categories => \@categories); $c->render(template => 'swin-edit'); }; post '/swin-edit' => sub ($c) { return $c->render_error('Access denied', 403) unless $c->is_admin; my $new_data = $c->param('swin_data'); unless ($new_data) { return $c->render_error('No data provided', 400); } my @categories; my $current_category = -1; foreach my $line (split /\n/, $new_data) { my ($type, $content) = split /\|/, $line, 2; if ($type eq 'category') { push @categories, { title => $content, links => [] }; $current_category++; } elsif ($type eq 'link' && $current_category >= 0) { my ($name, $url) = split /\|/, $content, 2; push @{ $categories[$current_category]->{links} }, { name => $name, url => $url }; } } if ($c->save_swin_data(\@categories)) { $c->redirect_to('/swin'); } else { $c->render_error('Failed to save data', 500); } }; app->start;