From 8a49af1ff24d9d6f2b565dd782eb3f2955c3990f Mon Sep 17 00:00:00 2001 From: Kaitlyn Parkhurst Date: Wed, 7 Sep 2022 04:44:24 +0000 Subject: [PATCH] Round on the web stuff. --- Web/cpanfile | 7 + Web/lib/MJB/Web.pm | 21 ++- Web/lib/MJB/Web/Controller/Auth.pm | 178 ++++++++++++++++++ Web/lib/MJB/Web/Controller/UserSettings.pm | 135 +++++++++++++ .../_base/bootstrap-datepicker.html.ep | 68 +++++++ Web/templates/_base/error_window.html.ep | 10 + Web/templates/_base/form/input.html.ep | 7 + Web/templates/_base/head.html.ep | 30 +++ Web/templates/_base/sidebar.html.ep | 70 +++++++ Web/templates/_base/topbar.html.ep | 15 ++ Web/templates/auth/forgot.html.ep | 35 ++++ Web/templates/auth/login.html.ep | 32 ++++ Web/templates/auth/register.html.ep | 45 +++++ Web/templates/auth/reset.html.ep | 38 ++++ Web/templates/layouts/standard.html.ep | 6 +- Web/templates/root/about.html.ep | 9 + Web/templates/root/contact.html.ep | 10 + Web/templates/root/index.html.ep | 4 +- Web/templates/root/pricing.html.ep | 73 +++++++ .../user_settings/change_password.html.ep | 45 +++++ Web/templates/user_settings/profile.html.ep | 43 +++++ 21 files changed, 876 insertions(+), 5 deletions(-) create mode 100644 Web/lib/MJB/Web/Controller/Auth.pm create mode 100644 Web/lib/MJB/Web/Controller/UserSettings.pm create mode 100644 Web/templates/_base/bootstrap-datepicker.html.ep create mode 100644 Web/templates/_base/error_window.html.ep create mode 100644 Web/templates/_base/form/input.html.ep create mode 100644 Web/templates/_base/head.html.ep create mode 100644 Web/templates/_base/sidebar.html.ep create mode 100644 Web/templates/_base/topbar.html.ep create mode 100644 Web/templates/auth/forgot.html.ep create mode 100644 Web/templates/auth/login.html.ep create mode 100644 Web/templates/auth/register.html.ep create mode 100644 Web/templates/auth/reset.html.ep create mode 100644 Web/templates/root/about.html.ep create mode 100644 Web/templates/root/contact.html.ep create mode 100644 Web/templates/root/pricing.html.ep create mode 100644 Web/templates/user_settings/change_password.html.ep create mode 100644 Web/templates/user_settings/profile.html.ep diff --git a/Web/cpanfile b/Web/cpanfile index dac8feb..a2cd2ee 100644 --- a/Web/cpanfile +++ b/Web/cpanfile @@ -1,4 +1,11 @@ requires 'Mojolicious'; requires 'Mojo::Pg'; requires 'Mojo::File'; +requires 'DateTime'; requires 'DateTime::Format::Pg'; +requires 'Try::Tiny'; + +# Remove when moving mail to its own package. +requires 'Email::Sender::Simple'; +requires 'Email::Sender::Transport::SMTP'; +requires 'Email::MIME::Kit'; diff --git a/Web/lib/MJB/Web.pm b/Web/lib/MJB/Web.pm index 298ea6a..bf04b14 100644 --- a/Web/lib/MJB/Web.pm +++ b/Web/lib/MJB/Web.pm @@ -18,7 +18,7 @@ sub startup ($self) { push @{$self->commands->namespaces}, 'MJB::Web::Command'; $self->helper( db => sub { - return state $db = WeightGrapher::DB->connect($config->{database}->{mjb}); + return state $db = MJB::DB->connect($config->{database}->{mjb}); }); # Standard router. @@ -67,6 +67,25 @@ sub startup ($self) { $r->get ( '/pricing' )->to( 'Root#pricing' )->name('show_pricing' ); $r->get ( '/contact' )->to( 'Root#contact' )->name('show_contact' ); + # User registration, login, and logout. + $r->get ( '/register' )->to( 'Auth#register' )->name('show_register' ); + $r->post ( '/register' )->to( 'Auth#do_register' )->name('do_register' ); + $r->get ( '/login' )->to( 'Auth#login' )->name('show_login' ); + $r->post ( '/login' )->to( 'Auth#do_login' )->name('do_login' ); + $auth->get( '/logout' )->to( 'Auth#do_logout' )->name('do_logout' ); + + # User Forgot Password Workflow. + $r->get ( '/forgot' )->to('Auth#forgot' )->name('show_forgot' ); + $r->post( '/forgot' )->to('Auth#do_forgot' )->name('do_forgot' ); + $r->get ( '/reset/:token' )->to('Auth#reset' )->name('show_reset' ); + $r->post( '/reset/:token' )->to('Auth#do_reset' )->name('do_reset' ); + + # User setting changes when logged in + $auth->get ( '/profile' )->to('UserSettings#profile' )->name('show_profile' ); + $auth->post( '/profile' )->to('UserSettings#do_profile' )->name('do_profile' ); + $auth->get ( '/password' )->to('UserSettings#change_password' )->name('show_change_password' ); + $auth->post( '/password' )->to('UserSettings#do_change_password' )->name('do_change_password' ); + } diff --git a/Web/lib/MJB/Web/Controller/Auth.pm b/Web/lib/MJB/Web/Controller/Auth.pm new file mode 100644 index 0000000..f7d13ac --- /dev/null +++ b/Web/lib/MJB/Web/Controller/Auth.pm @@ -0,0 +1,178 @@ +package MJB::Web::Controller::Auth; +use Mojo::Base 'Mojolicious::Controller', -signatures; +use Try::Tiny; +use DateTime; +use Email::Sender::Simple qw( sendmail ); +use Email::Sender::Transport::SMTP; +use Email::MIME::Kit; + +sub show_register ( $c ) { + +} + +sub do_register ( $c ) { + $c->stash->{template} = 'auth/register'; + + my $name = $c->stash->{form_name} = $c->param('name'); + my $email = $c->stash->{form_email} = $c->param('email'); + my $password = $c->stash->{form_password} = $c->param('password'); + my $p_confirm = $c->stash->{form_password_confirm} = $c->param('password_confirm'); + + push @{$c->stash->{errors}}, "Name is required" unless $name; + push @{$c->stash->{errors}}, "Email is required" unless $email; + push @{$c->stash->{errors}}, "Password is required" unless $password; + push @{$c->stash->{errors}}, "Confirm Password is required" unless $p_confirm; + + return if $c->stash->{errors}; + + push @{$c->stash->{errors}}, "Password and confirm password must match" + unless $p_confirm eq $password; + + push @{$c->stash->{errors}}, "Password must be at least 8 characters" + unless length($password) >= 8; + + return if $c->stash->{errors}; + + my $person = try { + $c->db->storage->schema->txn_do( sub { + my $person = $c->db->resultset('Person')->create({ + email => $c->param('email'), + name => $c->param('name'), + }); + $person->new_related('auth_password', {})->set_password($c->param('password')); + return $person; + }); + } catch { + push @{$c->stash->{errors}}, "Account could not be created: $_"; + }; + + return if $c->stash->{errors}; + + $c->session->{uid} = $person->id; + + $c->redirect_to( $c->url_for( 'dashboard' ) ); +} + +sub login ( $c ) { + + if ( $c->stash->{person} ) { + $c->redirect_to( $c->url_for( 'show_dashboard' ) ); + } +} + +sub do_login ( $c ) { + $c->stash->{template} = 'auth/login'; + + my $email = $c->stash->{form_email} = $c->param('email'); + my $password = $c->stash->{form_password} = $c->param('password'); + + my $person = $c->db->resultset('Person')->find( { email => $email } ) + or push @{$c->stash->{errors}}, "Invalid email address or password."; + + return 0 if $c->stash->{errors}; + + $person->auth_password->check_password( $password ) + or push @{$c->stash->{errors}}, "Invalid email address or password."; + + return 0 if $c->stash->{errors}; + + $c->stash->{person} = $person; + + $c->session->{uid} = $person->id; + + $c->redirect_to( $c->url_for( 'show_dashboard' ) ); +} + +sub do_logout ( $c ) { + undef $c->session->{uid}; + $c->redirect_to( $c->url_for( 'show_login' ) ); +} + +sub show_forgot ( $c ) { } + +sub do_forgot ( $c ) { + $c->stash->{template} = 'auth/forgot'; + + my $email = $c->stash->{form_email} = $c->param('email'); + + my $person = $c->db->resultset('Person')->find( { email => $email } ) + or push @{$c->stash->{errors}}, "There is no account with that email address."; + + return 0 if $c->stash->{errors}; + + # Make a token & send the email TODO + my $token = $person->create_auth_token( 'forgot' ); + + my $mkit_path = $c->config->{mkit_path}; + my $transport = Email::Sender::Transport::SMTP->new(%{$c->config->{smtp}}); + + my $kit = Email::MIME::Kit->new({ source => "$mkit_path/forgot_password.mkit" } ); + + my $message = $kit->assemble( { + send_to => $email, + link => 'https://' . $c->config->{domain} . "/reset/$token" + }); + + sendmail( $message, { transport => $transport } ); + + # Let the user know the next steps. + $c->stash->{success} = 1; + $c->stash->{success_message} = 'Please check your email for a password reset link.';; + + # Clear the form. + $c->stash->{form_email} = ''; +} + +sub show_reset ( $c ) { } + +sub do_reset ( $c ) { + $c->stash->{template} = 'auth/reset'; + + my $token = $c->param('token'); + my $password = $c->stash->{form_password} = $c->param('password'); + my $confirm = $c->stash->{form_password_confirm} = $c->param('password_confirm'); + + push @{$c->stash->{errors}}, "Password is required" unless $password; + push @{$c->stash->{errors}}, "Confirm Password is required" unless $confirm; + + return if $c->stash->{errors}; + + push @{$c->stash->{errors}}, "Password and confirm password must match" + unless $confirm eq $password; + + push @{$c->stash->{errors}}, "Password must be at least 8 characters" + unless length($password) >= 8; + + return if $c->stash->{errors}; + + my $lower_time = DateTime->now; + $lower_time->subtract( minutes => 60 ); + + my $record = $c->db->auth_tokens->search( { + token => $token, + scope => 'forgot', + 'me.created_at' => { '>=', $lower_time }, + }, { prefetch => 'person' })->first; + + push @{$c->stash->{errors}}, "This token is not valid." + unless $record; + + return 0 if $c->stash->{errors}; + + # Change the user's password. + $record->person->auth_password->update_password( $password ); + + # Log the user into the account + $c->session->{uid} = $record->person->id; + + # Delete this token. + $record->delete; + + # Send them to the dashboard. + $c->redirect_to( $c->url_for( 'show_dashboard' ) ); +} + +1; + + + diff --git a/Web/lib/MJB/Web/Controller/UserSettings.pm b/Web/lib/MJB/Web/Controller/UserSettings.pm new file mode 100644 index 0000000..2430dbf --- /dev/null +++ b/Web/lib/MJB/Web/Controller/UserSettings.pm @@ -0,0 +1,135 @@ +package MJB::Web::Controller::UserSettings; +use Mojo::Base 'Mojolicious::Controller', -signatures; + +sub profile ( $c ) { + $c->stash->{form_name} = $c->stash->{person}->name; + $c->stash->{form_email} = $c->stash->{person}->email; +} + +sub do_profile ( $c ) { + $c->stash->{template} = 'user_settings/profile'; + + my $name = $c->stash->{form_name} = $c->param('name'); + my $email = $c->stash->{form_email} = $c->param('email'); + my $password = $c->stash->{form_password} = $c->param('password'); + + # Populate errors if we don't have values. + push @{$c->{stash}->{errors}}, "You must enter your name" unless $name; + push @{$c->{stash}->{errors}}, "You must enter your email" unless $email; + push @{$c->{stash}->{errors}}, "You must enter your password" unless $password; + + # Bail out if we have errors now. + return 0 if $c->stash->{errors}; + + $c->stash->{person}->auth_password->check_password( $password ) + or push @{$c->stash->{errors}}, "You must enter your current login password correctly."; + + # Bail out if we have errors now. + return 0 if $c->stash->{errors}; + + $c->stash->{person}->name( $name ); + $c->stash->{person}->email( $email ); + + $c->stash->{person}->update; + + # Let the user know the action was successful. + $c->stash->{success} = 1; + $c->stash->{success_message} = "Your records were updated."; +} + +sub change_password ( $c ) { } + +sub do_change_password ( $c ) { + $c->stash->{template} = 'user_settings/change_password'; + + # Get the values the user gave for the password change. + my $password = $c->stash->{form_password} = $c->param('password'); + my $new_pass = $c->stash->{form_new_password} = $c->param('new_password'); + my $confirm = $c->stash->{form_password_confirm} = $c->param('password_confirm'); + + # Populate errors if we don't have values. + push @{$c->{stash}->{errors}}, "You must enter your current password" unless $password; + push @{$c->{stash}->{errors}}, "You must enter your new password" unless $new_pass; + push @{$c->{stash}->{errors}}, "You must enter your new password again to confirm" unless $confirm; + + # Bail out if we have errors now. + return 0 if $c->stash->{errors}; + + $c->stash->{person}->auth_password->check_password( $password ) + or push @{$c->stash->{errors}}, "You must enter your current login password correctly."; + + # Bail out if we have errors now. + return 0 if $c->stash->{errors}; + + push @{$c->stash->{errors}}, "Password and confirm password must match" + unless $new_pass eq $confirm; + + push @{$c->stash->{errors}}, "Password must be at least 8 characters" + unless length($new_pass) >= 8; + + # Bail out if we have errors now. + return if $c->stash->{errors}; + + # We can update the password now. + $c->stash->{person}->auth_password->update_password($new_pass); + + # Let the user know the action was successful. + $c->stash->{success} = 1; + $c->stash->{success_message} = "Your password was updated."; + + # Clear the form values on success. + $c->stash->{form_password} = ""; + $c->stash->{form_new_password} = ""; + $c->stash->{form_password_confirm} = ""; +} + +sub subscription ($c) { + my $status = $c->param('status'); + + # No status=, the user themself requested this page. + if ( ! $status ) { + return; + } + + # Status isn't successful, tell the user they could try agan. + if ( $status ne 'success' ) { + push @{$c->stash->{errors}}, "Subscription wasn't successful."; + return; + } + + my $session_id = $c->param('session_id'); + + my $customer_id = $c->ua->get( $c->config->{stripe_backend} . '/stripe/session-to-customer?session_id=' . $session_id )->result->json->{customer_id}; + + # Store the customer id along side the user in the DB. + if ( $customer_id ) { + $c->db->storage->schema->txn_do( sub { + $c->stash->{person}->stripe_customer_id( $customer_id ); + $c->stash->{person}->is_subscribed( 1 ); + $c->stash->{person}->update; + }); + } + + $c->stash( + success => 1, + success_message => 'Thank you for signing up!', + ); +} + +# Send to stripe to signup for the subscription +sub do_subscription ($c) { + my $lookup_key = $c->param('lookup_key'); + my $url = $c->ua->get( $c->config->{stripe_backend} . '/stripe/get-checkout-link?lookup_key=' . $lookup_key )->result->json->{url}; + + $c->redirect_to( $url ); +} + +# Send to stripe to manage the subscription +sub do_subscription_manage ($c) { + my $url = $c->ua->get( $c->config->{stripe_backend} . '/stripe/get-portal-link?customer_id=' . $c->stash->{person}->stripe_customer_id )->result->json->{url}; + + $c->redirect_to( $url ); +} + +1; + diff --git a/Web/templates/_base/bootstrap-datepicker.html.ep b/Web/templates/_base/bootstrap-datepicker.html.ep new file mode 100644 index 0000000..3d6e779 --- /dev/null +++ b/Web/templates/_base/bootstrap-datepicker.html.ep @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Web/templates/_base/error_window.html.ep b/Web/templates/_base/error_window.html.ep new file mode 100644 index 0000000..84fdeb2 --- /dev/null +++ b/Web/templates/_base/error_window.html.ep @@ -0,0 +1,10 @@ +% if ( $c->stash->{errors} ) { + +% } diff --git a/Web/templates/_base/form/input.html.ep b/Web/templates/_base/form/input.html.ep new file mode 100644 index 0000000..ae2b7f1 --- /dev/null +++ b/Web/templates/_base/form/input.html.ep @@ -0,0 +1,7 @@ + +
+ + +
<%= $help %>
+
+ diff --git a/Web/templates/_base/head.html.ep b/Web/templates/_base/head.html.ep new file mode 100644 index 0000000..18cf12f --- /dev/null +++ b/Web/templates/_base/head.html.ep @@ -0,0 +1,30 @@ + + + + TodayChecklist<%= $title ? " - " . $title : "" %> + + + + + + + + + + + + + + + + + + + + + + %= include '_base/bootstrap-datepicker' diff --git a/Web/templates/_base/sidebar.html.ep b/Web/templates/_base/sidebar.html.ep new file mode 100644 index 0000000..4cc96d6 --- /dev/null +++ b/Web/templates/_base/sidebar.html.ep @@ -0,0 +1,70 @@ + + + diff --git a/Web/templates/_base/topbar.html.ep b/Web/templates/_base/topbar.html.ep new file mode 100644 index 0000000..d28ce00 --- /dev/null +++ b/Web/templates/_base/topbar.html.ep @@ -0,0 +1,15 @@ + + diff --git a/Web/templates/auth/forgot.html.ep b/Web/templates/auth/forgot.html.ep new file mode 100644 index 0000000..d6adac9 --- /dev/null +++ b/Web/templates/auth/forgot.html.ep @@ -0,0 +1,35 @@ +% layout 'standard', title => 'Forgot Password', sb_active => 'forgot'; + + +

Reset Password

+ +

If you have forgotten the password for your account, please enter the email address below and you will get a link to reset your password.

+ +% if ( $c->stash->{success} ) { + +% } + +% if ( $c->stash->{errors} ) { + +% } + +
+ + <%= include '_base/form/input', type => 'email', name => 'email', + title => 'Email Address', + help => '', + value => $c->stash->{form_email} + %> + + +
+ diff --git a/Web/templates/auth/login.html.ep b/Web/templates/auth/login.html.ep new file mode 100644 index 0000000..a6dd7b8 --- /dev/null +++ b/Web/templates/auth/login.html.ep @@ -0,0 +1,32 @@ +% layout 'standard', title => 'Register', sb_active => 'login'; + +

Login

+ +% if ( $c->stash->{errors} ) { + +% } + +
+ + <%= include '_base/form/input', type => 'email', name => 'email', + title => 'Email Address', + help => '', + value => $c->stash->{form_email} + %> + + <%= include '_base/form/input', type => 'password', name => 'password', + title => 'Password', + help => '', + value => $c->stash->{form_password} + %> + + +
+ diff --git a/Web/templates/auth/register.html.ep b/Web/templates/auth/register.html.ep new file mode 100644 index 0000000..f0e7104 --- /dev/null +++ b/Web/templates/auth/register.html.ep @@ -0,0 +1,45 @@ +% layout 'standard', title => 'Register', sb_active => 'register'; + + +

Create an account

+ +% if ( $c->stash->{errors} ) { + +% } + +
+ + <%= include '_base/form/input', type => 'text', name => 'name', + title => 'Your name', + help => '', + value => $c->stash->{form_name} + %> + + <%= include '_base/form/input', type => 'email', name => 'email', + title => 'Email Address', + help => '', + value => $c->stash->{form_email} + %> + + <%= include '_base/form/input', type => 'password', name => 'password', + title => 'Password', + help => '', + value => $c->stash->{form_password} + %> + + <%= include '_base/form/input', type => 'password', name => 'password_confirm', + title => 'Confirm Password', + help => '', + value => $c->stash->{form_password_confirm} + %> + + +
+ diff --git a/Web/templates/auth/reset.html.ep b/Web/templates/auth/reset.html.ep new file mode 100644 index 0000000..3de88a3 --- /dev/null +++ b/Web/templates/auth/reset.html.ep @@ -0,0 +1,38 @@ +% layout 'standard', title => 'Reset Password', sb_active => 'forgot'; + +

Reset Password

+ +% if ( $c->stash->{success} ) { + +% } + +% if ( $c->stash->{errors} ) { + +% } + +
+ + <%= include '_base/form/input', type => 'password', name => 'password', + title => 'Enter your new password', + help => '', + value => $c->stash->{form_password} + %> + + <%= include '_base/form/input', type => 'password', name => 'password_confirm', + title => 'Confirm your new password', + help => '', + value => $c->stash->{form_password_confirm} + %> + + + +
diff --git a/Web/templates/layouts/standard.html.ep b/Web/templates/layouts/standard.html.ep index 5129944..da0c87a 100644 --- a/Web/templates/layouts/standard.html.ep +++ b/Web/templates/layouts/standard.html.ep @@ -3,7 +3,7 @@ - TodayChecklist<%= $title ? " - " . $title : "" %> + MyJekyllBlog<%= $title ? " - " . $title : "" %>