Commit graph

70 commits

Author SHA1 Message Date
William Carroll
2632dc10fd Ensure /accept-invitation is POST and not GET
Debugged this bug!
2020-08-02 18:00:05 +01:00
William Carroll
90d1451895 Render "<count> days until" for upcoming trips
Per the assignment spec.
2020-08-02 17:45:34 +01:00
William Carroll
e9e84f6a08 Support POST /accept-invitation
Allow users to accept invitations that we email to them.

TL;DR:
- I learned how to write FromHttpApiData instances, which allows me to
  parse/validate data at the edges of my application; this substantially cleans
  up my Handler code.
2020-08-02 16:30:28 +01:00
William Carroll
25334080b9 Support POST /invite
Allow Admin accounts to invite users to the application.
2020-08-02 16:07:35 +01:00
William Carroll
fe609bbe58 Support CRUDing records on Admin page
TL;DR:
- Prefer the more precise verbiage, "Accounts", to "Users"
- Add username field to Trip instead of relying on session.username
- Ensure that decodeRole can JD.fail for invalid inputs
2020-08-02 15:15:01 +01:00
William Carroll
81c3db20d4 Allow managers to CRUD all account types (not just admins)
Per the assignment's instructions.
2020-08-02 14:31:00 +01:00
William Carroll
803db7a5b2 Support printing user's itinerary
- Define print.css with media=print type (note: could've been handled with
  @media queries)
- Define printPage port to interop with window.print() JS function
- Support UI.wrapNoPrint to wrap components with a the no-print CSS
2020-08-02 14:23:38 +01:00
William Carroll
699892883c Support deleting trips from the client
TL;DR:
- Ensure Types.TripPK in Types.hs uses Calendar.Day for startDate
- Prefer verbiage "GotCreateTrip" to "CreatedTrip"
- Extend Utils.deleteWithCredentials to accept a body parameter
- Support a delete button in the UI
2020-08-02 11:16:24 +01:00
William Carroll
d5bc6f963d Protect views from a State with an empty Session
This should simplify UserHome among other views.
2020-08-02 11:13:54 +01:00
William Carroll
57b6472e2f Define defaults for init in State.elm
Problem: When I'm working on a feature, I save my code, and elm-live reloads the
browser. This is usually good, except that the application state is
reinitialized, which usually means that the view changes.

I defined two state configurations, and I expect to define more:
- prod: The initial state for the application
- userHome: The state I'd like to use when developing a feature for the UserHome
  page.

Idea: For more ad-hoc configurations, I can store the application state in
LocalStorage and restore it in between page refreshes.
2020-08-02 10:51:26 +01:00
William Carroll
ac9629cad0 Assign fixed width to error banners
This makes the banners easier to center horizontally.
2020-08-02 10:33:42 +01:00
William Carroll
249e3113ff Support creating Trips from the frontend
*sigh* ... spent way too much time encoding/decoding date types...

I need my database, server, client, and JSON need to agree on types.

TL;DR:
- Add CSS for elm/datepicker library
- Create Common.allErrors to display UI errors
- Prefer Data.Time.Calendar.Day instead of newtype Date wrapper around Text
2020-08-01 23:04:06 +01:00
William Carroll
54eb29eae0 Prefer RecordWildCard syntax for toFields functions
Refactoring old code to conform to the latest fashion.
2020-08-01 12:29:31 +01:00
William Carroll
83f4f8e9d6 Prevent non-admins from creating Manager or Admin accounts
Client-side, I'm not exposing the role option to users. Server-side, I'm
asserting that requests to create Manager and Admin accounts are attempted by
users with a session tied to an admin account.
2020-08-01 11:48:55 +01:00
William Carroll
a3732300e1 Add exhaustive patterns to FromJSON Role instance
When someone enters something like role=mgr, return a helpful error message to
the user.

Note: I should enable the exhaustive patterns check for GHC.
2020-08-01 11:46:41 +01:00
William Carroll
9666d5dce1 Support sign-up
Toggle b/w logging in or signing up.

TL;DR:
- From my previous submission's feedback, disallow users from signing themselves
  up as admins, managers; I just removed the UI element altogether, even though
  the server still support this (TODO)
2020-07-31 19:33:18 +01:00
William Carroll
cf5d211477 Support UI.disabledButton
While this isn't necessary, it tidies up the code a bit.
2020-07-31 19:32:30 +01:00
William Carroll
4d30a80487 Support UI.textButton
Create a text-only button.
2020-07-31 19:32:14 +01:00
William Carroll
421c71c892 Support a basic client-side login flow
I will need to remove some of the baggage like:

- Scrub any copy about restaurants
- delete Restaurant.elm
- Change Owner.elm -> Manager.elm
2020-07-31 18:57:35 +01:00
William Carroll
29a00dc571 Configure non-simple CORS server-side
@dmjio says (probably correctly) that it's best to just serve the client from
the server and circumvent CORS issues altogether.

One day I will set that up. For now, this works... *sigh*
2020-07-31 18:31:52 +01:00
William Carroll
cdaa449670 Prefer PUT to PATCH
It was always a PUT. Nothing to see here, folks.
2020-07-31 18:30:03 +01:00
William Carroll
35b218c543 Return a JSON Session on a successful POST /login
This will make the UX on a the client-side smoother.
2020-07-31 18:30:03 +01:00
William Carroll
c8ed6e51fe Read CLIENT and SERVER endpoints from .envrc
In the spirit of DRY.
2020-07-31 18:28:15 +01:00
William Carroll
1d7c77f51d Support POST /unfreeze
Allow admins and managers to unfreeze accounts that we froze for security
reasons.
2020-07-31 11:37:45 +01:00
William Carroll
43eff5f1d0 Prefer RecordWildCards for FromJSON instances
Stylistically, I think this looks cleaner.
2020-07-31 11:27:47 +01:00
William Carroll
ed557fb6be Support PATCH /trips
Support a top-level PATCH request to trips that permits any admin to update any
trip, and any user to update any of their trips.

I'm using Aeson's (:?) combinator to support missing fields from the incoming
JSON requests, and then M.fromMaybe to apply these values to any record that
matches the primary key.

See the TODOs that I introduced for some shortcomings.
2020-07-31 11:25:36 +01:00
William Carroll
7d64011cbd Protect GET /trips with a session cookie
When an admin requests /trips, they see all of the trips in the Trips
table. When a user requests /trips, they see only their trips.
2020-07-31 10:55:10 +01:00
William Carroll
75437b01b6 Check for GTE instead of GT
Somebody incremenet the total number of off-by-one errors that I've made in my
career. I think the current count is 99... or is it 100? 101? Who knows?!
2020-07-30 19:53:46 +01:00
William Carroll
ea31a01497 Debug LoginAttempts.increment
When this was an UPDATE statement with a WHERE clause, and the LoginAttempts
table was vacant, nothing would happen. Thankfully, SQLite supports an UPSERT
clause so that I can INSERT a new record or UPDATE conditionally.

And the best part is: it works!
2020-07-30 19:52:37 +01:00
William Carroll
8ebc89b44b Remove erroneous parens around columns in SELECT statement
These were causing runtime errors... whoops!
2020-07-30 19:52:04 +01:00
William Carroll
6ecab8c3a6 Prefer SELECT (a,b,c) to SELECT *
"SELECT *" in SQL may not guarantee the order in which a record's columns are
returned. For example, in my FromRow instances for Account, I make successive call

The following scenario silently and erroneously assigns:

firstName, lastName = lastName, firstName

```sql
CREATE TABLE People (
  firstName TEXT NOT NULL,
  lastName TEXT NOT NULL,
  age INTEGER NOT NULL,
  PRIMARY KEY (firstName, lastName)
)
```

```haskell
data Person = Person { firstName :: String, lastName :: String, age :: Integer }

fromRow = do
  firstName <- field
  lastName  <- field
  age       <- field
  pure Person{..}

getPeople :: Connection -> IO [Person]
getPeople conn = query conn "SELECT * FROM People"
```

This silently fails because both firstName and lastName are Strings, and so the
FromRow Person instance type-checks, but you should expect to receive a list of
names like "Wallace William" instead of "William Wallace".

The following won't break the type-checker, but will result in a runtime parsing
error:

```haskell
-- all code from the previous example remains the same except for:

fromRow = do
  age       <- field
  firstName <- field
  lastName  <- field
```

The "SELECT *" will return records like (firstName,lastName,age), but the
FromRow instance for Person will attempt to parse firstName as
Integer.

So... what have we learned? Prefer "SELECT (firstName,lastName,age)" instead of
"SELECT *".
2020-07-30 18:52:45 +01:00
William Carroll
dec8890190 Verify users' email addresses when they attempt to sign-up
Lots of changes here:
- Add the GET /verify endpoint
- Email users a secret using MailGun
- Create a PendingAccounts table and record type
- Prefer do-notation for FromRow instances (and in general) instead of the <*>
  or a liftA2 style. Using instances using `<*>` makes the instances depend on
  the order in which the record's fields were defined. When combined with a
  "SELECT *", which returns the columns in whichever order the schema defines
  them (or depending on the DB implementation), produces runtime parse errors
  at best and silent errors at worst.
- Delete bill from accounts.csv to free up the wpcarro@gmail.com when testing
  the /verify route.
2020-07-30 18:38:46 +01:00
William Carroll
30838b8df7 Add Haskell client library for MailGun
Whichever package is on nixpkgs right now is broken, so I'm using `fetchGit` and
`callCabal2nix`.

Create Email module exposing a simplifies `send` function that partially applies
some of the configuration options.
2020-07-30 17:07:49 +01:00
William Carroll
b6e8389edd Read env variables using envy library
Using my dear friend's, dmjio's, excellent library, envy -- to read and parse
variables from the system environment.

I added and git-ignored the .envrc file that contains API secrets. I'm using
Envy to read these values, so that I don't hard-code these values into the
source code.
2020-07-30 13:58:50 +01:00
William Carroll
385164c6af Authorize endpoints
If I ever fully learn `servant-auth`, I'll probably recognize how naive this
hand-rolled solution is. But it works! And the code is pretty declarative, which
I like.
2020-07-30 10:23:55 +01:00
William Carroll
ca26fcd523 Debug erroneous table name
"Session" doesn't exist, but "Sessions" does.
2020-07-30 09:51:32 +01:00
William Carroll
fdd51f626c Fully support login, logout
Refactor my handlers to use the `Handler a` type instead of `IO a`; this allows
me to throwError inside of handlers that Servant properly handles. Previously I
was creating 500 errors unnecessarily.
2020-07-29 20:26:23 +01:00
William Carroll
ab12be7840 Support looking up a session by its UUID
We need to read a session from the session table using its UUID.
2020-07-29 20:21:56 +01:00
William Carroll
16f50e33bc Prefer deleting sessions by their UUID
Instead of deleting them by usernames.
2020-07-29 20:21:29 +01:00
William Carroll
c4a090e558 Support reading / writing cookies in API
Update my API type and handler types to reflect which handlers read and write
cookies.

TODO:
- Actually read from and write to Set-Cookie header
- Returning `pure NoContent` breaks my types, so I'm returning `undefined` now
2020-07-29 14:14:47 +01:00
William Carroll
9f70cb2c61 Add boilerplate for Google sign-in
For more information, read:
https://developers.google.com/identity/sign-in/web/sign-in?authuser=1

TODO: Use Elm ports or something similar to properly interop with the onSignIn
and signOn functions defined in index.html.
2020-07-29 10:13:19 +01:00
William Carroll
289cae2528 Add Elm boilerplate to project
Create a top-level client directory to store my Elm boilerplate.
2020-07-29 09:51:18 +01:00
William Carroll
cf6c8799ab Restrict users from multiple failed login attempts
I'm not resetting the failed LoginAttempt count, which is a low priority for
now, but necessary eventually.
2020-07-28 21:33:58 +01:00
William Carroll
f051b0be0b Check passwords in /login
TL;DR:
- Since POST /login is more rigorous, our accounts.csv needs to contain validly
  hashed passwords; you can use tests/create-accounts.sh to create dummy
  accounts

I still need to test the login flow and support:
- Tracking failed attempts (three maximum)
- Verifying accounts by sending emails to the users
2020-07-28 18:48:38 +01:00
William Carroll
90a521c78f Create Utils module for (|>) operator
For the past 3-4 Haskell projects on which I've worked, I've tried to habituate
the usage of the (&) operator, but I find that -- as petty as it may sound -- I
don't like the way that it looks, and I end up avoiding using it as a result.

This time around, I'm aliasing it to (|>) (i.e. Elixir style), and I'm hoping to
use it more.
2020-07-28 18:47:40 +01:00
William Carroll
191205acac Create populate.sqlite3 to simplify README
To make my life easier, I created a small sqlite3 script to populate our
database.
2020-07-28 18:47:40 +01:00
William Carroll
36a2fea686 Create Sessions table
TL;DR:
- Create Sessions SQL schema
- Create Sessions module
- Introduce UUID dependency
2020-07-28 18:40:17 +01:00
William Carroll
012296f156 Move SQL out of API and into separate modules
Create modules for each Table in our SQL database. This cleans up the handler
bodies at the expense of introducing more files and indirection.
2020-07-28 18:38:30 +01:00
William Carroll
b355664858 Support /login
Support basic authentication.

Note the TODOs that this commit introduces to track some of the remaining work.
2020-07-28 14:15:41 +01:00
William Carroll
b170be9375 Hash passwords when creating accounts
TL;DR:
- introduce the Cryptonite library
- Remove the redundant language extensions, imports, deps from Persistent
- Prefer NoContent return type for POST /accounts
- Define custom {To,From}JSON instances for Role
2020-07-28 12:51:17 +01:00