Continuing where we left off yesterday in part 2 on this series about building a new web software application, I wanted to take a moment to discuss part of the data model. In particular, I wanted to cover the user model, and how it will impact this application.
Roughly, we should know the following about each user:
- Username -- a unique ID by which we can refer to an individual account
- Email address -- we need a way of contacting the individual
- Password -- to validate each request
- Friends -- so we can share various notes
- Preferences -- some way of storing per-user configurations
- Level -- or priviledges, or administrative rights, etc., for select individuals
- Status -- to indicate whether or not the account is active, disabled, deleted, etc.
One option was to use the email address as a unique ID. This is what systems like Gmail do, but those systems are obviously set up to keep email addresses unique. Flickr uses your email address as the primary login key as well, but that forced them to create an awkward contrivance of having the user choose a public identifier for their account. Granted, logging in with an email address is probably easier for the user, but since we are going to request a different public identifier anyway (so the user does not need to reveal their address publicly to share notes) we may as well ask for that up front. Also, this makes it easier when the user needs to change their email address.
We can probably get away with just storing a hash of the password. In other words, when a user changes their password we will simply keep a MD5 or SHA1 hash of the string and discard the original. If the database is compromised the intruder will not gain a list of all the plain text passwords. This also means that we will never be able to tell the user what their password is, but that is a small price to pay for the additional security.
Authentication on each API call will be done by passing in both the username and the password on each request. There are more secure ways, of course. One could use a two-token authentication model or HTTP AUTH. However, a two-token system would require the client to maintain state in a more involved way, and HTTP AUTH could potentially result in a awkward user experience when dialog boxes popped up in the browser. Moreover, both schemes would make it harder for scripts and whatnot to use the backend web services. As a result, I'm leaning toward following Flickr's lead and simply requesting the username and password on each (stateless) request.
While I don't expect that this application will need a full friend-of-a-friend framework, I do think it would be useful to say that certain notes are one of (a) private, (b) visible to friends, (c) visible to everyone. In fact, there should also be a distinction between read access and write access for any given note. At this time, however, my inclination is to not bother with a UNIX-style group system, and hope that a friends model will suffice.
The preferences data can probably be stored as simple key/value string pairs. But this will need to be examined in more detail as we see what type of information is required.
The priviledge system can be arbitrarily complex. In reality, it will probably only need to be sophisticated enough to say whether or not a user is an administrator. WordPress uses a notion of "level" to express this with an integer from 1 through 10, though I'm not sure if any features make use of this degree of granularity.
Also, I have decided to make the initial version of this application "invite only," similar to Gmail. There are a number of reasons for this. First, and most obvious, it helps control the growth of the service.
But more important, this is a good way of combating abuse. For instance, if you see that a user is abusing the system you can close down their account. But if they can simply sign up for another one than you are constantly playing a game of cat and mouse against the abuser. However, if you know where the user got their invite then you can cut them off at the head. But by always requiring invitations you have an explicit tree of responsibility -- there is always someone that can be held accountable, going all the way back to the first account. (BTW, I believe this is why Gmail is still, and will always be, inivitation only. This is partly evidenced by the fact they they effectively offer you unlimited invites, but still keep the model alive.)
Certain other bits of data -- such as how a user wants his or her name displayed, may fit into the preferences, or may belong as part of the user itself.
Off the top of my head, these are some of the API calls that we will need for users:
/users/user/[username/invite/[email_address]?username=[...]&password=[...]
(GET)/users/user/[username]?invite_id=[...]
(PUT)/users/user/[username]?username=[...]&password=[...]
(GET, POST, DELETE)/users/user/[username]/reset
(GET)/users/user/[username]/preferences?username=[...]&password=[...]
(GET, PUT, POST, DELETE)/users/user/[username]/friends?username=[...]&password=[...]
(GET, PUT, POST, DELETE)
I tried to twist my mind around enough to make API calls that look like real REST. Most "REST" APIs, such as Amazon and Flickr, aren't really that REST-ful. However, since we have the luxury of starting from scratch, it is at least worth considering the right ways to do things.
Most of the API calls take parameters. Each of those parameters is technically optional -- however, without those optional parameters (such as username and password), the system may potentially behave in different ways (such as denying the caller access). For example, calling
/users/user/dewitt/preferences
won't return anything unless you also pass a parameter of my (or an adminstrator's) username and password. In other words, the optional parameters of "username" and "password" are orthogonal to the path of the API call, but they will influence what results it returns.So here's a concrete example of how it all works:
Say I'm DeWitt, and I have a username of "dewitt" and a password of "untonet". I would like to invite my friend Kristin, at kristin@unto.net (not a real address) to join the site. I would then make an API call of:
/users/user/dewitt/invite/kristin@unto.net?username=dewitt&password=untonet
(GET)
The system will then check to see if the username and password I supplied (dewitt and untonet) have permissions to make an API call of /users/user/dewitt/invite. Since they match, and dewitt has some invitations left, the system then generates a unique token (say, 123456) and stores it in the database. The system then sends an email off to kristin@unto.net saying that she's been invited and that she can sign up at a particular HTML page (part of the client GUI).
Kristin checks her email and sees the invitation. She then navigates to the GUI which asks her to fill in some information consisting of her desired email address, desired username, password, etc. For example, perhaps she picks "kristin" as a username and "secret" as a password. The GUI now makes an API call of:
/users/user/kristin?invite_id=123456
(PUT)
And the body of the PUT is an XML (or similar) document containing all of the user information that Kristin supplied. If the invite id is valid, the new username is available, and all of the required data is present, then a new account will be created, the database update to reflect the state of the invitation, and a confirmation email will be sent out to both DeWitt and Kristin.
Kristin gets her confirmation email and can now make API calls of her own. For instance, to retrieve her new user data, she can call:
/users/user/kristin?username=kristin&password=secret
(GET)
This will return an XML document containing all of the user information for "kristin" that she is allowed to see. Or, if she wants to see her list of friends, she can call:
/users/user/kristin/friends?username=kristin&password=secret
(GET)
And since I am automatically added to Kristin's list of friends (since I invited her), part of the result set will be an element containing
"/users/user/dewitt"
. This is just a high-level synopsis of the user model and the corresponding web services API. I am sure that many things will change during the implementation, but I am hoping that the foundation is sound. If you see any areas that need to be addressed before I continue, please let me know.
Much more to come...