This is a s2cgi-based set of CGI scripts for managing card data for the Cide Site.
This API is 100% JSON. The JSON structure for the cards, along with various other project-related data, can be found over in the public GDrive folder.
Programming Language
s2, using the s2cgi CGI framework.
Storage
This service uses s2's sqlite3 loadable module to for storage. Here's the db schema.
Error Reporting
The backend always tries to respond with a valid JSON response, even when responding with HTTP codes 400 or higher (errors). If a response object has an exception
property, that exception will have a message
property which (hopefully) describes the problem. The message will normally be a string, but the framework allows it to be of any type, so it "might not" be a string (but usually will be). The backend normally "scrubs" exceptions to remove an stack trace/file/line info, as that's security-relevant, but that feature can be disabled when running it on a local web server, so that it sends complete exception objects, in all their glory.
If a response ever arrives without a JSON body, it's because some lower level of the infrastructure failed, e.g. bootstrapping of the CGI process, or even-lower-level code failed in a fatal way (e.g. a failed C-level or s2-level assertion). It is not expected that this actually happen in practice, but... yeah.
Loading/saving Cards
Loading
- A single card: GET
/card?id=X/Y
, whereX
the card's deck abbreviation (case-insensitive) andY
is the card number. (Reminder to self:#
was the preferred separator but when using it from the browser it must be escaped, which is annoying.) Or pass it a card's case-sensitive UID (which must not contain a/
character, or it will be confused as the first form). This method responds with a single card object using the schema demonstrated below. - A deck, optionally including its cards: GET
/deck?id=X
, where X is the deck's abbreviated name (case-insensitive) or its UID (case-sensitive). Pass the parametercards
, with no equal sign or value, or with "=true", to include the deck's cards in the result (an array property namedcards
). The cards are currently ordered by number, but this may later be changed to sort by their name. Don't rely on a specific sorting of either the decks or the cards. - All decks: GET
/decks
. By default the cards are not included - use the same approach as for/deck
to get the cards.
Saving
To save a card or deck, simply POST a JSON-format object using the schema described... somewhere... else... to the /card
resp. /deck
route. (This API does not use the PUT HTTP method because CGI does not define that method. "Pure REST" APIs which use PUT cannot be portably implemented as CGIs.) Both routes accept a single object or an array of such objects, and return the saved (and modified) object(s) in the same form.
If saving fails for any reason, e.g., due to a constraint violation or missing/invalid required property, an exception is thrown.
These requests modify the card object to set its last-updated time and, if needed. Because they are modified, the updated object(s) is/are returned by these methods. Note that saving carefully prunes any unknown properties, so the returned object will have any "extra" properties which arrived in the request stripped from it.
Authentication
Currently there is almost none. Anyone can save any cards. This approach has worked relatively well for me in past niche-market projects like this one. If it gets abused, it will either be turned off or some form of authentication will be added.
This API has login support but currently only distinguishes between admin and anonymous users. An admin user is permitted to do a few things which normal users are not:
- Overwrite "locked" cards (those which are "write-protected"). If a save request contain a valid admin authentication cookie then overwriting locked cards is permitted. Non-admin users will get an exception if they try to do so.
- Irrevocably delete cards and (if it's ever needed) decks. (Yes, a GET request can be used for deletion. So sue me, you REST addict!)
There are no high-level tools for setting up an admin user - it has to be done directly in the database using voodoo nobody on earth understands.
Log in: POST /admin/login
a request payload of {"user": "name", "password": "..."}
. The response, on success, will be an object with {"name":"username", "cideToken":"..."}
. That cideToken
part gets set as a cookie and is used to authenticate further admin-level requests.
Who Am I?: GET to /admin/whoami
returns the same structure as /admin/login
. If a valid auth token cookie is found, that user's login token and name are returned, otherwise an object with null
values is returned. This allows an app to determine whether its auth cookie is still valid, effectively performing a continuation of an older session.
Logout: POST or GET to /admin/logout
. If the auth cookie is set (or passed as a GET parameter or in the body of a POST'd JSON object), the current login will be invalidated. Regardless of success or failure (no auth token found), the same result as whoami
is returned.
JSON Structures
Card JSON Structure
{
"header": {
"name": "Propane Tanks",
//"fontSizePercent": 300, // OPTIONAL for the header (kinda).
"deck": string, // deck name abbreviation (not its UID)
"number": integer, // card number within that deck
"version": integer // version number of the card
},
"body": {
"fontSizePercent": 200, // described in the GDoc docs. REQUIRED for the body
"text": [
// string or array of strings (which get concatenated together).
// May contain certain specialized markup (documented elsewhere) but not HTML.
"When killed, roll 1d6: ",
"on {{dice 1-3}} (killed by melee weapon) or ",
"{{dice 1-5}} (ranged weapon or car) ",
"this zombie explodes with the effect of a Molotov. ",
"Attacker gains no XP from explosion kills."
]
},
"meta": { // optional "not-on-the-card" data
"uid": string, // stable unique identifier, distinct across all decks
"mtime": integer, // Unix Epoch timestamp of last database modification time
"contributor": string, // optional name/URL of the card's contributor
"locked": boolean // true if this card is "locked" (uneditable) in the db.
// locked has no effect when set/changed from the client except (someday) in admin mode
}
}
Deck JSON Structure
{
"name": string, // Unique name. e.g. "Zombie Abilities".
"abbr": string, // card ID/name abbreviation. e.g. "ZA".
// This is the key the related cards use to refer to the deck.
"cards": array, // list of card objects. Not all HTTP requests return these.
"meta": { // system-level state
"uid": string, // stable unique identifier
"mtime": integer, // Unix Epoch timestamp of last database modification time
"locked": boolean // true if this deck is "locked" (uneditable) in the db.
// The exact semantics of locked decks are still under consideration.
}
}