609 lines
18 KiB
Perl
609 lines
18 KiB
Perl
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) = @_;
|
|
<html>
|
|
<body>
|
|
<p>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.</p>
|
|
|
|
<p>El enlace de verificación es <a href="<%= $url %>"><%= $url %></a>.</p>
|
|
</body>
|
|
</html>
|
|
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;
|