AJAX password form validation

Hey all,

Before today, I’ve never really worked with AJAX, XML HTTP Requests, readyStates, form submissions, etc. And before a month or two ago, my Javascript knowledge was pretty abysmal. I’m learning a lot but the tutorials aren’t cutting it—I need a little help!

I’m working on a new portfolio and I have a case study which needs to be password protected. I’ve created a ‘guest’ user and modified the Kirby cookbook for authentication to allow people to access the case study. The username field is pre-filled and hidden on the front-end, so only the password is needed. With the controller I have, this all works fine.

What I would like to do now is:

  1. If the password is incorrect, display an error message below the input, without page refresh.
  2. If the password is correct, display a success message below the input, and then redirect to the unlocked page (same URL) after a timeout. (The template already checks whether a user is logged in, so this shouldn’t require any extra authentication.)

I think I’m pretty close, but there are gaps in my understanding of how all the bits communicate with each other, so I’m working in the dark. Anyone who can shine a light would be much appreciated :slight_smile:

Please excuse any rookie mistakes below :stuck_out_tongue:

Also, I’m not using jQuery on this project, so vanilla JS only please!

// Login panel (embedded on templates/project.php,
// if the project is marked as requiring a password and
// no user is logged in)

<form id="protected" method="post">
  <input type="hidden" id="username" name="username" value="guest">
  <div class="password-submit">
    <label for="password">Passphrase</label>
    <div class="password-input-wrapper">
      <?php snippet('icons/lock') ?>
      <input type="text" id="password" name="password" maxlength="48" placeholder="Welcome to the truth">
    </div>
  </div>
  <div class="message"><?php if($message) echo $message ?></div>
  <input class="visually-hidden" type="submit" name="login" value="Go">
</form>
// templates/project.json.php

<?php echo json_encode($response);
// submit.js

var form = document.getElementById('protected')

if (form.addEventListener) {
  form.addEventListener('submit', handlePasswordSubmit, false)
} else if (form.attachEvent) {
  form.attachEvent('onsubmit', handlePasswordSubmit)
}

function handlePasswordSubmit (e) {
  e.preventDefault()

  var form = document.getElementById('protected')
  var url = form.action + '.json'
  var usernameField = document.getElementById('username')
  var passwordField = document.getElementById('password')
  var usernameText = usernameField.value
  var passwordText = passwordField.value

  var xhr = new XMLHttpRequest()
  xhr.open('POST', url, true)
  xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
  xhr.send({
    username: usernameText,
    password: passwordText
  })

  xhr.onreadystatechange = function () {
    if (xhr.readyState === XMLHttpRequest.DONE) {
      if (xhr.status === 200) {
        // Check if password is a match: if no, insert
        // error message text into .message div
        // If yes, insert success message
        // into .message div and redirect after 4000ms
      } else {
        // If the server fails, I think - what would be the cause of this?
        // What error should be displayed to the user?
        console.log('Something went wrong. Please try again.')
      }
    }
  }
}
// controllers/project.php

<?php

return function($site, $pages, $page) {
  if(r::is('post') and get('login')) {
    $invalidMessage = 'Incorrect passphrase. Try again, and be sure to include spaces.';
    $successMessage = 'Correct! Redirecting&hellip;';

    if($user = $site->user(get('username')) and $user->login(get('password'))) {
      $message = $successMessage;
      return;
    } else {
      $message = $invalidMessage;
    }

    // If the username is constant, do I need to include it?
    // Does the message go into this array, or should it be elsewhere?
    $response = array(
      'username' => get('username'),
      'password' => get('password'),
      'message'  => $message
    );
  }

  return $response;
};

If you would use jQuery I could give you an answer but ajax with vanilla js is not really intuitive.
If you speak german you can read this Tutorial as a startingpoint to understand how a loginform with ajax has to work: https://www.html-seminar.de/ajax-einfuehrung.htm
If not say something and I will take a look to an english one.

But the normal way is to send your form data to your controller, have in mind that a controllerfile will not work as a ajaxscript.
For this you can use a routing and set up your controller as a plugin like here: Where can I save my PHP-Files for AJAX?

cheers

There are several issues here that you should resolve before digging deeper:

project.php controller:

  • you have to check for an Ajax request, not a post request
  • $response is not defined for a standard get request, only within your if-statement

form in project.php template:

  • no action attribute (although you try to get its value in your JS
  • $message is not defined (only inside the $response variable, then you have to get the message from the $reponse variable)

** in JS**

 var url = form.action + '/.json'; // i.e. with the `/`

There’s probably more…

@texnixe There are definitely more. :wink: Thanks for taking the time to respond.

I’ll take some time to go through your points soon and see how far I get.

Going back to basics, I tried to get all of the PHP working correctly again first, since the original code I posted was broken. This is based on the authentication and Ajax form validation tutorials.

  <form id="protected" method="post" action="<?php echo $page->url() ?>">
    <input type="hidden" id="username" name="username" value="guest">
    <div class="password-submit">
      <label for="password">Passphrase</label>
      <div class="password-input-wrapper">
        <?php snippet('icons/lock') ?>
        <input type="text" id="password" name="password" maxlength="48" placeholder="Welcome to the truth">
      </div>
    </div>
    <?php if (isset($response['success'])): ?>
      <div class="message"><?= $site->loginsuccessmessage() ?></div>
    <?php elseif (isset($response['error'])): ?>
      <div class="message"><?= $site->loginerrormessage() ?></div>
    <?php endif ?>
    <input class="visually-hidden" type="submit" name="login" value="Go">
  </form>
// controllers/project.php

<?php

return function($site, $pages, $page) {
  $response = array();

  if(r::is('post') and get('login')) {
    $response['username'] = get('username');
    $response['password'] = get('password');
    if($user = $site->user($response['username']) and $user->login($response['password'])) {
      $response['success'] = true;
    } else {
      $response['error'] = true;
    }
  }

  return compact('response');
};

@texnixe You mentioned checking for an AJAX request here ^ instead of a POST request: would that prevent logging in for users who have Javascript disabled?

With the above code, error messages display (after a page refresh) and success messages should display in theory, but because the browser reloads the page by default and then the user is logged in, the project page is displayed and the login form is hidden.

I believe that project.json is working as intended, although I’m not sure how to check it because the data is populated on POST, so visiting projectname.json just displays an empty array.

In my mind I imagine AJAX being a layer of sugar on top of this foundation: one that improves the user experience but will not break the login functionality if it is lost. A) Is this possible, or does integrating AJAX require significant changes to the code above? B) Can you briefly outline the steps needed to get there? I don’t need a copy-paste solution, just a direction or key points to look into. I’ve been watching some tutorials on AJAX to get a better understanding, but I’m still struggling in pairing it with Kirby.

Thanks for your help :slight_smile:

Controller:

<?php

return function($site, $pages, $page) {
    $response = [];
   
    if(r::is('post')) {
  
    if(($user = $site->user('guest')) && $user->login(get('password'))) { // you might as well hardcode your user here
      $response['success'] = 'Correct! Redirecting&hellip;';
    } else {
      $response['error'] = 'Incorrect passphrase';
    }

  
  }

  return compact('response');
};

json template

<?php echo json_encode($response);

Script:

var form = document.getElementById('protected');

if (form.addEventListener) {
  form.addEventListener('submit', handlePasswordSubmit, false);
} else if (form.attachEvent) {
    .attachEvent('onsubmit', handlePasswordSubmit)
}

function handlePasswordSubmit (e) {
  e.preventDefault();

  var form = document.getElementById('protected');
  var data = new FormData(form);
  var url = form.action + '.json';
  console.log(url);
  var usernameField = document.getElementById('username');
  var passwordField = document.getElementById('password');
  var usernameText = usernameField.value;
  var passwordText = passwordField.value;

  var xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);
  xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  xhr.send(data);

  xhr.onreadystatechange = function () {
    if (xhr.readyState === XMLHttpRequest.DONE) {
      if (xhr.status === 200) {
        console.log(xhr.response);
        // Check if password is a match: if no, insert
        // error message text into .message div
        // If yes, insert success message
        // into .message div and redirect after 4000ms
      } else {
        console.log(xhr.response);
        // If the server fails, I think - what would be the cause of this?
        // What error should be displayed to the user?
      }
    }
  }
}

Form:

  <?php if(!$site->user()): ?>
  <form id="protected" method="post" action="<?= $page->url() ?>">
  <!-- why an input for the username at all, hardcode it in the controller instead , removed -->
  <div class="password-submit">
    <label for="password">Passphrase</label>
    <div class="password-input-wrapper">
     
      <input type="password" id="password" name="password" maxlength="48" placeholder="Welcome to the truth">
    </div>
  </div>
  <div class="message"><?php if(isset($response['error'])) echo $response['error'] ?></div>
  <input class="visually-hidden" type="submit" name="login" value="Go">
</form>
<?php else: ?>
<!-- content -->
<?php endif ?>

Then redirect to the content or inject the error message.

2 Likes

THANK YOU SO MUCH @texnixe for helping guide me toward a working solution. Everything is up and running! It works great.

new FormData(form) was the missing link. Once that was in place, pretty much everything fell into place. I’m sure there are tons of optimizations that could still be made to my code, but I feel like I have a better understanding of form submissions now. Thank you :slight_smile: