package MyRedland::Controller::User; use v5.34.1; use strict; use warnings; use Mojo::Base 'Mojolicious::Controller'; use Digest::SHA qw/sha512_hex/; use Data::Dumper; use DateTime; use Crypt::URandom qw/urandom/; use Crypt::Bcrypt qw/bcrypt/; use Capture::Tiny qw/capture/; use MyRedland::Categories; use MyRedland::Lusers; use Mojo::Template; use Mojo::URL; use Mojo::Util qw/url_escape/; use MyRedland::Mail; use MyRedland::Products; use MyRedland::Stripe; use Path::Tiny; my $PROJECT_ROOT = path(__FILE__)->parent->parent->parent->parent; my $UPLOADS = $PROJECT_ROOT->child('uploads'); $UPLOADS->mkpath; my $mt = Mojo::Template->new( auto_escape => 1 ); sub login_get { my $self = shift; if ( my $user = $self->current_user ) { $self->_already_logged_in; return; } my $categories = MyRedland::Categories->new->Retrieve; $self->stash( categories => $categories ); $self->stash( current_slug => $categories->{index}{slug} ); $self->render; } sub setup_avatar { my $self = shift; my $upload = $self->req->upload('file'); my $user = $self->current_user; if ( !defined $user ) { $self->render( status => 401, json => { status => 401, code => 'NOTLOGGED', description => 'No estás loggeado.', } ); return; } if ( !$user->verified ) { $self->render( status => 401, json => { status => 401, code => 'NOTVALIDYET', description => 'Tu usuario no está validado, comprueba tu correo electrónico.', } ); return; } if ( !defined $upload ) { $self->render( status => 400, json => { status => 400, code => 'NOFILE', description => 'No se recibió ningún fichero. (¿Usaste un multipart/form-data?)' } ); return; } my $tempdir = Path::Tiny->tempdir; my $file = $tempdir->child('tempfile'); $upload->move_to($file); system 'file', $file; my ( $stdout, $stderr, $error ) = capture { system qw/identify -format "%wx%h"/, $file; }; if ( $error != 0 ) { $self->render( status => 400, json => { status => 400, code => 'INVFILE', description => 'Archivo inválido. (El comando identify no lo reconoció)' } ); return; } my ( $width, $height ) = $stdout =~ /^"(\d+)x(\d+)"$/; if ( $width < 50 ) { $self->render( status => 400, json => { status => 400, code => 'SMALLFILE', description => 'El archivo debería tener 50x50 píxeles como mínimo.' } ); return; } if ( $width != $height ) { $self->render( status => 400, json => { status => 400, code => 'NOTASQUARE', description => 'La imagen debe ser cuadrada, debe tener el mismo numero de pixeles de ancho que de alto.' } ); return; } my $converted_file = $tempdir->child('converted.png'); ( $stdout, $stderr, $error ) = capture { system 'convert', $file, $converted_file; }; if ( $error != 0 ) { say STDERR $stdout; say STDERR $stderr; $self->render( status => 500, json => { status => 500, code => 'SERVERCONVERSIONFAILED', description => 'El servidor no fué capaz de convertir el fichero a png, prueba a enviar otro formato.', } ); return; } my $sha512 = sha512_hex( $converted_file->slurp ); $user->avatar($sha512); my $users_dao = MyRedland::Lusers->new( app => $self->app ); $user = $users_dao->update( user => $user, fields => [qw/avatar/] ); system 'cp', $converted_file, $UPLOADS->child($sha512); $self->render( status => 200, json => { status => 200, code => 'SUCESS', description => 'Your avatar was correctly setup.', } ); } sub get_avatar { my $self = shift; my $user = $self->current_user; if ( !defined $user ) { $self->render( status => 401, text => 'Still not logged in.' ); return; } if ( !$user->avatar ) { $self->render( status => 400, text => 'Avatar still not setup.' ); return; } $self->render( format => 'png', data => $UPLOADS->child( $user->avatar )->slurp ); } sub setup_avatar_get { my $self = shift; my $user = $self->current_user; if ( !defined $user ) { $self->_must_be_logged; return; } if ( !$user->verified ) { $self->_must_be_verified; return; } $self->render; return; } sub _must_be_verified { my $self = shift; $self->render( status => 400, text => 'Debes verificar tu correo antes de hacer eso' ); } sub _already_logged_in { my $self = shift; $self->render( text => 'Ya estás loggeado.', status => 403 ); } sub login { my $self = shift; if ( my $user = $self->current_user ) { $self->_already_logged_in; return; } my $username = $self->param('username'); my $password = $self->param('password'); my $users_dao = MyRedland::Lusers->new( app => $self->app ); my $user = $users_dao->find_by_username( username => $username ); my $ip = $self->tx->remote_address; if ( !$user ) { say "No such user for login $username from $ip."; $self->_invalid_login; return; } if ( !$user->check_password($password) ) { say "Invalid login attempt for @{[$user->username]} from $ip."; $self->_invalid_login; return; } $self->_create_session_for_user($user); $self->res->headers->location('/perfil'); $self->render( status => 302, text => 'Exito logueandose, redirigiendo...' ); } sub _invalid_login { my $self = shift; $self->render( text => 'Login invalido.', status => 401 ); } sub sign_up_get { my $self = shift; if ( my $user = $self->current_user ) { $self->_already_logged_in; return; } my $categories = MyRedland::Categories->new->Retrieve; $self->stash( categories => $categories ); $self->stash( current_slug => $categories->{index}{slug} ); $self->render; } sub sign_up { my $self = shift; if ( my $user = $self->current_user ) { $self->_already_logged_in; return; } my $username = $self->param('username'); my $password = $self->param('password'); my $repeat_password = $self->param('repeat_password'); my $email = $self->param('email'); if ( !defined $username || length($username) < 5 ) { $self->render( text => 'El nombre de usuario debe ser mayor a 4 caracteres.', status => 400 ); return; } if ( $username =~ /\// ) { $self->render( text => '"/" es un carácter prohibido en el nombre de usuario', status => 400, ); return; } if ( !defined $password || !defined $repeat_password || length $password < 10 ) { $self->render( text => 'La contraseña debe contener al menos 10 caracteres.', status => 400 ); return; } if ( $password =~ /^\d*$/ ) { $self->render( text => 'La contraseña no puede estar compuesta solo por números debido a que es extremadamente inseguro.', status => 400 ); return; } if ( !defined $email || $email !~ /@.*\.\w+$/ ) { $self->render( text => 'El email que has enviado no parece valido, contacta con contact@owlcode.tech desde dicho email si estás seguro que lo es.', status => 400 ); return; } if ( $password ne $repeat_password ) { $self->render( text => 'La contraseña no coincide con la repetición.', status => 400 ); return; } my $password_hash = bcrypt( $password, '2b', 12, urandom(16) ); my $users_dao = MyRedland::Lusers->new( app => $self->app ); my $user; eval { $user = $users_dao->create( username => $username, password => $password_hash, email => $email ); }; if ($@) { say STDERR $@; if ( $@ =~ /duplicate.*username/ ) { $self->render( text => 'El nombre de usuario ya existe, prueba a recuperar contraseña.', status => 400 ); return; } if ( $@ =~ /duplicate.*email/ ) { $self->render( text => 'El email ya existe, prueba a recuperar contraseña.', status => 400 ); return; } $self->render( text => 'Error desconocido de base de datos contacta con contact@owlcode.tech.', status => 500 ); return; } $self->_create_session_for_user($user); say "Created user @{[$user->username]} with email @{[$user->email]}."; if ( $user->verified ) { $self->res->headers->location('/perfil/configura-tu-avatar'); } else { my $mailer = MyRedland::Mail->new( app => $self->app ); my $current_url = $self->req->url->to_abs; $current_url->path(''); $current_url->query(''); my $url = Mojo::URL->new( "${current_url}usuario/@{[url_escape($user->username)]}/verificacion" ); $url->query( a => $user->mail_verification_payload ); my $html_email = $mt->render( <<'EOF', $url ); % my ($url) = @_;

Hemos recibido una solicitud de registro en Redland Official con tu dirección de correo, si no has sido tú por favor ignora este email.

El enlace de verificación es <%= $url %>.

EOF my $text_email = <<"EOF"; Hemos recibido una solicitud de registro en Redland Official con tu dirección de correo, si no has sido tú por favor ignora este email. El enlace de verificación es $url. EOF $mailer->sendmail( to => $user->email, subject => 'Verifica tu email.', html => $html_email, text => $text_email, ); $self->res->headers->location('/perfil/verifica-el-correo'); } $self->render( text => 'Login success.', status => 302 ); } sub user_verification { my $self = shift; my $username_to_verify = $self->param('username'); my $verification_payload = $self->param('a'); my $user = $self->current_user; if ( !defined $user ) { my $url = Mojo::URL->new('/login'); my $current_url = $self->req->url; $url->query( redirect => $current_url ); $self->res->headers->location($url); $self->render( text => 'Debes loggearte antes.', status => 302 ); return; } if ( $user->username ne $username_to_verify ) { $self->render( text => 'Has tratado de verificar una cuenta estando loggeado con otra, este incidente será reportado para futuras acciones de moderación.', status => 403 ); say STDERR "@{[$user->username]} attempted to verify the account $username_to_verify, possible hacking or multiaccount."; return; } if ( $verification_payload ne $user->mail_verification_payload ) { $self->render( text => 'La url de verificación no es correcta, prueba de nuevo.', status => 400 ); return; } my $current_date = DateTime->now; if ( $current_date > $user->mail_verification_expiration ) { $self->render( text => 'Este enlace ya expiró, genera otro desde tu perfil', status => 400 ); return; } $user->verified(1); my $users_dao = MyRedland::Lusers->new( app => $self->app ); $user = $users_dao->update( user => $user, fields => [qw/verified/] ); say "@{[$user->username]} was verified."; $self->res->headers->location('/perfil/configura-tu-avatar'); $self->render( text => 'Usuario verificado con exito.', status => 302 ); } sub _create_session_for_user { my $self = shift; my $user = shift; my $session = $self->session; if ( !defined $user ) { die "User is not defined."; } $session->{user_uuid} = $user->uuid; } sub _must_be_logged { my $self = shift; $self->render( text => 'Debes estar loggeado para acceder esta parte.', status => 401 ); } sub logout_get { my $self = shift; my $user = $self->current_user; if ( !defined $user ) { $self->_must_be_logged; return; } my $categories = MyRedland::Categories->new->Retrieve; $self->stash( categories => $categories ); $self->stash( current_slug => $categories->{index}{slug} ); $self->render; } sub profile { my $self = shift; my $user = $self->current_user; if ( !defined $user ) { $self->_must_be_logged; return; } my $categories = MyRedland::Categories->new->Retrieve; $self->stash( categories => $categories ); $self->stash( current_slug => $categories->{index}{slug} ); $self->render; } sub logout { my $self = shift; my $user = $self->current_user; if ( !defined $user ) { $self->_must_be_logged; return; } if ( defined $self->param('yes') ) { for my $key ( keys %{ $self->session } ) { # We do not want to store any session data after logout because of the security implications. # We use session root keys for things such as storing the current SubscriptionOrder. delete $self->session->{$key}; } } $self->res->headers->location('/'); $self->render( text => 'Action succeded', status => 302 ); } sub subscription_options { my $self = shift; my $user = $self->current_user; if ( !defined $user ) { $self->_must_be_logged; return; } if ( !$user->verified ) { $self->_must_be_verified; return; } $self->render; } sub subscribe_get { my $self = shift; my $user = $self->current_user; if ( !defined $user ) { $self->_must_be_logged; return; } if ( !$user->verified ) { $self->_must_be_verified; return; } my $product = $self->param('product'); if ( !defined $product ) { $self->render( text => 'Debes indicar un producto.', status => 400 ); return; } my $products_dao = MyRedland::Products->new; $product = $products_dao->find_by_id( id => $product ); if ( !defined $product ) { $self->render( text => 'No se encontró ese producto.', status => 404 ); return; } my $stripe = MyRedland::Stripe->new( app => $self->app ); if ( !defined $user->stripe_customer_id ) { $user = $stripe->create_customer_for_user($user); } my $payment_intent = $self->current_payment_intent; if ( $self->_check_new_so_conditions_subscription_get($product) ) { say STDERR "Creating new SubscriptionOrder for @{[$user->username]}."; my $subscription_orders_dao = MyRedland::SubscriptionOrders->new( app => $self->app ); my $pi = $stripe->create_payment_intent( user => $user, price => $product->price, off_session => 0 ); my $payment_intent_id = $pi->{id}; my $client_secret = $pi->{client_secret}; my $subscription_order = $subscription_orders_dao->create( product => $product, user => $user, payment_intent_id => $payment_intent_id, client_secret => $client_secret, renew_auto => 1, save_card => 1, paid => 0, to_pay => $product->price ); $self->current_order($subscription_order); } my $nonce = unpack 'H*', urandom(15); $self->res->headers->content_security_policy( "default-src 'self' 'nonce-$nonce'; connect-src 'self' https://api.stripe.com https://maps.googleapis.com; frame-src https://js.stripe.com https://hooks.stripe.com; script-src 'self' 'nonce-$nonce' https://js.stripe.com https://maps.googleapis.com" ); $self->render( product => $product, subscription_order => $self->current_order, nonce => $nonce, config => $self->app->config ); } sub _check_new_so_conditions_subscription_get { my $self = shift; my $product = shift; my $payment_intent = $self->current_payment_intent; my $user = $self->current_user; return !defined $self->current_order || $product->id ne $self->current_order->product->id || $user->uuid ne $self->current_order->user->uuid || $payment_intent->{status} eq 'succeeded'; } sub payment_success_get { my $self = shift; my $current_order = $self->current_order; if ( !defined $current_order ) { $self->render( text => 'No hay ninguna orden guardada en tu navegador, esto no quiere decir que tu orden no exista, contacta con contact@owlcode.tech en caso de duda.', status => 400 ); return; } my $payment_intent = $self->current_payment_intent; if (!defined $payment_intent) { $self->render( text => 'No hay orden de pago, esto es un error del servidor.', status => 500, ); return; } $self->render( so => $current_order, pi => $payment_intent ); } 1;