Friday, February 16, 2018

getting ready to invite people (14)

This booking system is for an invite-only event - a scout camp - which is why I've made a form for editing your details, but not for registering from scratch. I'm expecting the database to be loaded with basic details of each invitee (from an existing membership database), and then invitees to be emailed a link to fill out the form.

The registration links that exist now aren't suitable for sending out: registation/1 contains a number that is almost definitely sequential, and is trivial editable to view and modify other people's registrations.

I don't want to go down the road of building a username/password system that will need a lot of human support to deal with lost passwords, etc.

Instead, I'll use the technique username/password systems often use to let you reset your password when you can't remember it but when you do have access to your email: I want to send a link with a large random-looking value, intended to be unguessable, instead of a sequentially numbered one - something like registration/24f556856f4cfce0e08e17769465d92f.

So add a new string column to the database to store this key called nonce (although it arguably isn't a nonce).

The existing records need something done to them: either we need to make nonce nullable so those records can have no key (and be inaccessible); or we need to invent a value for them (This is doable in postgres but I'm not going to do it because this series is meant to be about Haskell); or we could do the migration in Haskell code (but postgresql-simple-migrations doesn't support that from the command line) or we can set them to some magic constant (such as the empty string "") which reeks of 1970s data processing.

If we were starting from a completely clean database, we wouldn't have this choice to make: we could insist every record has a nonce, and make the column NOT NULL. But I promised myself at least (and maybe you) that we wouldn't need to wipe out the database any more after implementing migrations.

So I'll make the nonce nullable, and the relevant bits of Haskell code will have to deal with that. (you make a field nullable by not writing NOT NULL as part of the type: well done SQL for supporting non-nullable values, but boo sql for making it non-default.

Here's a migration to add as migrations/0003-nonce.sql:

ALTER TABLE registration ADD COLUMN nonce TEXT;

TEXT columns in the registration have so far always been converted to/from Haskell String values inside the Registration type. That will sort-of work in some cases for nonce but not always: String doesn't have a way to represent a NULL - so we won't be able to write out NULL values to the database (which probably won't be a problem for us) and reading in NULL values will cause some kind of explosion (which will probably be a problem for us).

The obvious type to use instead is Maybe String, and indeed that works out of the box.

So onto the end of Registration in src/Registration.hs, add in a new field: nonce :: Maybe String

None of the SQL code needs changing now because of the generics magic in previous posts, but there's still that pesky applicative in registrationDigestiveForm which needs this sticking on the end (if you added the nonce field on the end of the Registration type):

...
  <*> "nonce" .: DF.optionalString (nonce initial)

We can't use DF.string here because that is for String, not Maybe String but digestive-functors has a suitable optionalString. If it didn't, it would be a very straightforward wrapper to build around DF.string.

If I'm going to invite people by email, I'm also going to need one other critical field: their email address. I'll call that email because I'm expecting that there might be other email addresses involved as user requirements change.

So migrations/0004-email.sql can be:

ALTER TABLE registration ADD COLUMN email TEXT;

... and we can add that new field onto the Registration type:

...
  email :: Maybe String
  }

... and onto registrationDigestiveForm:

...
    <*> "email" .: DF.optionalString (email initial)

We could stick some kind of email validation onto this, instead of directly using DF.optionalString but good luck.

While we're adding fields, let's add yet another field which is going to track the completion status of this form: I want to know if a registration is New, or if they have been Invited, or if they have Saved the form, or if they have Completed registration, or if their registration has been cancelled. Those capital letters are there to suggest that I'm going to use a single character field for this in the database.

Here are the relevant code fragments - hopefully you can figure out where they go:

ALTER TABLE registration ADD COLUMN status CHAR(1);
UPDATE registration SET status = 'N' WHERE status IS NULL;
ALTER TABLE registration ALTER COLUMN swim SET NOT NULL;

...

  status :: String
...
  <*> "status" .: nonEmptyString (Just $ status initial)

Luckily for this field, there's a meaningful default value: N for new, so the migration sets that status for every existing record, and we don't need to use Maybe.

In the next posts, I'll start using these fields: first to generate invitation URLs for new registrations, and then next to actually email those registrants.

No comments:

Post a Comment