$ npm install
# initialize sqlite database on disk
$ npm run schema
# run express server
$ npm run start
- I used UUIDs for user & event IDs instead of numeric auto-increment IDs. UUIDs are easier to shard than numeric auto-increment IDs and immune to enumeration attacks (if using UUIDv4 with good RNG). Before moving to production I would use an integer column type (or UUID column type if available) instead of a string column type to store the UUIDs more efficiently.
- For input validation I used regexes in the model layer. To improve the current validation code I would:
-
- Use a validation library (e.g. replace the email regex with an NPM module that does more robust email validation).
-
- Move the validation code into a separate validation layer, outside of the model code.
-
- Return multiple errors to the client instead of one error at a time. This becomes much easier if you use a data validation library instead of rolling your own validation as I did.
-
- I used SHA256(password, uuid, salt) for hashing the password. This should be replaced with bcrypt or a similar cryptographic hashing library before moving to production. I also added some basic password strength requirements (8 characters, 1 letter, 1 number).
- I created a
config.ts
file to store configuration values, but all of the values are currently hardcoded. The hardcoded values could exist as defaults (or moved to aconfig.example.ts
file) but should be overwritten by environment variables, command line arguments, or by using a package likedotenv
. This is especially true forsalt
, which is a secret value and should not be checked into the repo. - I used the low level
sqlite3
library for persistence. I may need to do something smarter than creating a single shareddb
object at the top level ofindex.ts
for concurrent requests. Also, forEvent.filter
it would have been nice to use an ORM to dynamically build up the query based on thecreated
column,userId
column, both columns, or neither. As it is I had to write out the logic for eachSELECT
statement. - Right now there is no authorization for any of the endpoints. One simple way to add authorization would be to require clients to send an API key in a header, then check that incoming API key against an
api_keys
table in the database. - I added Mocha to the
package.json
, but didn't have time to add any unit or integration tests. I've written up a manual test plan below with examples of the output returned.
Here are the steps I used to validate the REST endpoints on the command line using cURL. I would use a similar set of test cases for unit and integration tests (verify validation is firing, verify you can write to the database, verify you can read data back, etc.).
$ npm install
$ npm run schema
Initializing schema for app.db.
Schema created.
Done.
$ npm run start
App is listening on port 8000.
$ curl http://127.0.0.1:8000/
{
"resources": [
"/users",
"/events"
]
}
# validation error
$ curl -d '{"email": "test1%foobah,com", "password": "foo", "phoneNumber": "555"}' -H "Content-Type: application/json" http://127.0.0.1:8000/users
{
"result": "failure",
"error": "Invalid email address."
}
# success
$ curl -d '{"email": "test1@foobah.com", "password": "foopass123", "phoneNumber": "555-123-9999"}' -H "Content-Type: application/json" http://127.0.0.1:8000/users
{
"result": "success"
}
# list all users
$ curl http://127.0.0.1:8000/users
{
"users": [
{
"id": "435951c0-2a78-11e9-b095-a5481bbaf0da",
"email": "test1@foobah.com",
"phoneNumber": "555-123-9999",
"created": 1549503162
}
]
}
# validation error
$ curl -d '{"userId": "435951c0-2a78-11e9-b095-a5481bbaf0da"}' -H "Content-Type: application/json" http://127.0.0.1:8000/events
{
"result": "failure",
"error": "The eventType field is required."
}
# success
$ curl -d '{"userId": "435951c0-2a78-11e9-b095-a5481bbaf0da", "eventType": "login"}' -H "Content-Type: application/json" http://127.0.0.1:8000/events
{
"result": "success"
}
# show all events
$ curl http://127.0.0.1:8000/events
{
"events": [
{
"id": "ba363e70-2a78-11e9-b095-a5481bbaf0da",
"userId": "435951c0-2a78-11e9-b095-a5481bbaf0da",
"eventType": "login",
"created": 1549503362
}
]
}
# filter events by a non-existent userId
$ curl http://127.0.0.1:8000/events?userId=zzz
{
"events": []
}
# filter events by the userId we used earlier
$ curl http://127.0.0.1:8000/events?userId=435951c0-2a78-11e9-b095-a5481bbaf0da
{
"events": [
{
"id": "ba363e70-2a78-11e9-b095-a5481bbaf0da",
"userId": "435951c0-2a78-11e9-b095-a5481bbaf0da",
"eventType": "login",
"created": 1549503362
}
]
}
# filter events by last 24 hours
$ curl http://127.0.0.1:8000/events?lastDay=yes
(same output as above)
# filter events by created timestamp in the past
$ curl http://127.0.0.1:8000/events?createdAfter=1549501958
(same output as above)
# filter events by created timestamp one day in the future
$ python3 -c "import time; print(int(time.time())+24*60*60)"
1549590058
$ curl http://127.0.0.1:8000/events?createdAfter=1549590058
{
"events": []
}