**Morbad Card Tools: Backend Service APIs** Wha? ======================================================================== This page documents the back-end JSON service APIs used by the [Morbad Card Tools][mct-site]. See also: [this tool's source repo][mct-repo-backend] API Basics ======================================================================== It's JSON and CGI All the Way Down ------------------------------------------------------------------------ This API is 100% JSON-over-HTTP. It is *not* [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)-compliant. It's implemented as a [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface), and CGI does not specify the full range of HTTP methods called for by REST. It uses only `GET` and `POST` methods. Though this implementation is a CGI, there is nothing about the API which depends on that. It would be straightforward to implement this same API using any halfway-capable back-end technology stack. We use CGI because it's convenient, reliable, and available in this project's web infrastructure (namely, an inexpensive shared hoster). POSTing Data ------------------------------------------------------------------------ APIs which accept `POST` data require that it be a UTF-8-encoded JSON object (not array). Not `application/x-www-form-urlencoded` (which is jQuery's default), nor any other weird encoding. i.e. post them a single JSON object string. If the client app (i.e. the browser) posts using UTF-8 (which they all(?) do (strangely, considering that JavaScript doesn't internally use UTF-8)) then there's nothing more one has to do with regard to the binary format of the data. When these docs refer to a "`POST` payload", they're referring to such an object. > ***Sidebar:*** the APIs which save cards via `POST` accept arrays > of cards as well as individual cards. The backend requires that inbound JSON have a `Content-Type` header of `application/json`. It optionally, for historical reasons, accepts `text/plain` or `application/javascript`, both of which are assumed to possibly contain JSON. (When that code was implemented, `application/json` was not yet as widely recognized as it is nowadays.) From JavaScript, all one has to do is set the request `Content-Type` header (for [XHR](https://en.wikipedia.org/wiki/XMLHttpRequest) that's done using the `setRequestHeader()` method) and post the result of calling `JSON.stringify(theRequestObject)`. TODO: demonstrate this with an XHR request. (The "real" client-side code uses a custom XHR wrapper which we don't want to demonstrate here because the reader would need to understand that wrapper's API.) Error Reporting ------------------------------------------------------------------------ The API 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 and script-level info (file/line/column), as that's security-relevant, but scrubbing can be disabled when running it on a local web server, so that it sends complete exception objects, in all their glory. (That configuration is part of the lower-level [s2cgi][] framework, not this API. By default it disables scrubbing only when serving from a host with a name suffix of `.local`, as that convention is what its developer uses for localhost Apache vhost aliases.) 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. APIs ======================================================================== Common Request Options ------------------------------------------------------------------------ The following URL arguments are accepted by all of the APIs (but don't necessarily make much sense for all of them): - `indent` (with no value) indents the JSON output using one hard tab per level. - `jsonp=string` wraps the response JSON in JSONP form using the given string as the wrapper name. e.g. `jsonp=foo.bar.baz` wraps the output in `foo.bar.baz(...the json...)`. Low-priority bug: if `jsonp` is specified, `indent` is apparently ignored. That shouldn't be the case. Authentication ------------------------------------------------------------------------ Currently there is *almost* none. Anyone can save any cards, with the exception of cards which are copyrighted by Goblinko (the creators of this fine game). Goblinko-official cards may only be modified by an admin user. This no-login-needed approach has worked surprisingly well in past niche-market projects like this one. If it gets abused, it will either be turned off or some form of user account system will be added. This API has login support but currently only distinguishes between admin and non-admin users. An admin user is permitted to do a few things which normal users are not: - Create/overwrite official Goblinko cards. If a `/save` request contain a valid admin authentication cookie then creating or overwriting Goblinko cards is permitted. - Irrevocably delete cards: `GET` or `POST` to `/admin/delete/CARDTYPE?id=...`. The `id` may (if it contains no slashes) optionally be supplied as a trailing path element or it may be contained in a POSTed JSON object: `{"id":"..."}`. (Yes, a GET request can be used for deletion. So sue me!) There are no high-level tools for setting up an admin user - it has to be done directly in the database (but see [user.s2](../../../finfo/user.s2) for a script which handles that). > ***Sidebar:*** the user credentials are stored as SHA3-256 hashes > created by combining the password and username with a > database-instance-specific random secret. At no point are they > recorded in plain-text form. ### Admin Login To log in as an admin, `POST` a JSON document with this structure: ```json { "user": "login name", "password": "..."} ``` On failure an exception object is returned. On success, an object is returned: ```json { "user": "name", "morbadToken": "..." // authentication token } ``` The API sets the `morbadToken`'s value as a cookie, so clients normally need not bother with it. It is provided in the JSON API solely for clients which don't support cookies. If cookies are not an option, that token must be submitted with each admin-level API request, either as a `GET` pararameter or as a `morbadToken:...` property in the `POST` request payload. How long the token is valid for is unspecified, but it tends to be long-lived. ### Who Am I? If a client has a `morbadToken` but does not know if it's valid, they can submit a `/admin/whoami` request, either with `GET` or `POST`, and submit their `morbadToken` value as a cookie, `GET` parameter, or in the body of a `POST` payload. The response object has an identical structure as the login response. If the token is valid, it will contain the user's name and the same `morbadToken` which was sent to it. If the token is missing or invalid then it responds with: ```json { "morbadToken": null, "user": null } ``` In practice, the first thing an HTTP-based client does when starting up is `GET /admin/whoami`, allowing the browser to submitt their token cookie (if any). If the returned object contains a non-`null` token value then the user is already/still logged in. ### Logout Simply `GET` or `POST` to `/admin/logout`, submitting the `morbadToken` using any of the approaches described for `/admin/whoami`. That will invalidate the token and respond with the same thing `whoami` does when the auth token is invalid. Fetching Cards ------------------------------------------------------------------------ - A single card: `GET` `/card-type?id=the-card-id`, where `card-type` is one of: (`skill`, `weakness`, `triumph`, `encounter`, `ffm`, `ffs`). Each method returns a single card object using the schema described [elsewhere in these docs](#card-structures). - All cards of a given type: `GET` `/skills` resp. `/weaknesses` resp. `/triumphs` resp. `/encounters` resp. `/ffms` resp. `/ffses`. Returns an array of card objects sorted by card name (case-insensitive, noting that Encounter cards can have either 1 or 2 names, and the sort is performed on the first name). - All cards: `GET` `/cards`. Returns an object in the form `{X:[...X cards...], ...}`, where `X` is a card type name with the same value as the corresponding single-card `GET` method, sorted by card name as described for the previous method. This structure gets expanded as new card types are added. The latter two options optionally accept the parameters `goblinko` or `homebrew`, with no value, to filter the list to include only official Goblinko cards resp. "homebrew" (fan-created) cards, noting that the database does not host Goblinko-official Encounter cards, and FFM/FFS cards are all homebrew (the have no counterparts in the core game). If passed both, they behave as if neither had been passed, returning both types of cards. The structure of the individual card types is described [elsewhere in these docs](#card-structures). Saving Cards ------------------------------------------------------------------------ To save a card, simply `POST` a JSON-format object using the schema described [elsewhere in these docs](#card-structures). (This API does *not* use the HTTP `PUT` method because CGI does not define that method. "Pure REST" APIs which use `PUT` cannot be portably implemented as CGIs.) The URI corresponds to the card type: `/skill`, `/weakness`, `/triumph`, `/encounter`, `/ffm`, `/ffs`. If a card has an `id` property then it will replace any existing card of the same type with that id, otherwise it will be assigned a new id. Exception: if the card is a Goblinko-official card, the backend will refuse to save it unless the user is logged in as an administrator. Likewise, a duplicate `name` field (or and/or `name2`, in the case of two-part Encounter cards), with a *different* card ID, will cause save to fail (even for an admin user). All card names must be unique for that type of card (two cards of different types may share a name). A number of card-specific validations are performed, and saving will fail if any of them are violated (or perceive themselves as having been violated). That said: the backend does not validate the *ranges* of most values, just the types. e.g. if a posted card has a semantically invalid `fontSizePercent` property, it doesn't care because it doesn't know what the valid/useful range for that property is (it only validates that it's an integer). If saving fails for any reason, e.g. due to a constraint violation or missing/invalid required property, an exception is thrown. The `/save` requests modify the card object to normalize its structure, set its last-updated time and, if needed, generate an ID (a random UUID). Because they are modified, the updated object is 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. ### Saving Multiple Cards Multiple cards of the same type can be saved via a single request by wrapping them in an array and `POST`ing that array. The cards are saved in a single database transaction, so if saving of one fails, they all fail. On success, these responses return an array of the normalized cards, exactly as described above, in the order they were provided in the input. Card Structures ------------------------------------------------------------------------ Rather than duplicate all of these docs again... The complete JSON schemas for the various card types are documented in [the main client-side documentation][mct-gdocs]. The backend API validates and enforces, insofar as feasible, data types but not the semantics of the content. In a few cases where schemas support arbitrary input, it can do no validation - it saves what it gets as-is. Implementation Details ======================================================================== The details in this section are not relevant for purposes of using the API. They're provided here solely for posterity's sake. Programming Language ------------------------------------------------------------------------ [s2](https://wanderinghorse.net/computing/cwal/s2/), using the [s2cgi][] CGI framework. It's not a blazingly fast back-end, as s2 prioritizes low memory consumption over speed, but it's easy to work with and is self-contained, in that it does not rely on any specific versions of third-party programming languages or libraries beyond the standard C library and sqlite3 (which is exceedingly API-stable). Storage ------------------------------------------------------------------------ This service uses [s2's sqlite3 loadable module](https://fossil.wanderinghorse.net/r/cwal/doc/ckout/s2/mod/sqlite3/) for storage. Here's [the db schema](../../../finfo/db/morbad.sql). [mct-site]: https://morbad.wanderinghorse.net [mct-repo-backend]: ../../../ [s2cgi]: https://fossil.wanderinghorse.net/r/s2cgi [mct-gdocs]: https://docs.google.com/document/d/12pFwBzuGUl6Ih9EcgXI6xa97-ahAS74GfchZ83caSg4/view