This is the template project that's checked out and configured when you run the bando-up command from ljsthw-bandolier. This is where the code really lives.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
bandolier-template/lib/api.js

303 lines
10 KiB

/*
__WARNING__: Early documentation. This should be treated as notes to
remind you of how to use the `api/` handlers with this, rather than
a full guide.
This is a convenience library that helps with normalizing JSON API
requests and responses with [Express.js](https://expressjs.com). The
`API` class contains useful functions for redirecting, building replies,
sending back errors, authenticating users, and validating forms. It is
primarily used in writing `api/` handlers.
## How `api/` Handlers Run
There's a command in `commands/api.js` that runs everything, and contains a lot
of tricks for Express.js that you should study. You run this with the `bando.js`
command runner like this:
```shell
node bando.js api
```
This command then imports all of the files in `api/` recursively, as well as
the `socket/` handlers, and sets them up to handle requests. Requests are mapped
by URL to `api/` by taking the `.js` off the file and using that as the URL. So
`api/running.js` becomes `/api/running`.
## Writing `api/` Handlers
The code in `api/` is mapped to URLs almost directly, so to create a new
URL just make a file in that directory. For example, if you want `/api/running`
as a check if your server is running, then create the file `api/running.js`.
The easiest way to make this file is to use the `node bando.js djent` command like this:
```shell
node bando.js djent --template static/djenterator/api.js --output api/running.js
```
This will use the template in `static/djenterator/api.js` to craft a starter file
for your `api/running.js`. If you want to change the output you can access
[http://127.0.0.1:5001/admin/#/djenterator/](http://127.0.0.1:5001/admin/#/djenterator/)
and visually choose the template to change, and edit the `.json` variables file it uses.
You should study this template output for all of the things you might need, although most
of it you'll strip out in this little tutorial.
## Handling Requests
To handle a `GET`, `POST`, or other request you simply create an exported function that
matches the lowercase version of the name:
```javascript
export const get = async (req, res) => {
// do you thing here
}
```
In the example file `api/running.js` you just created with `bando.js djent`
you should see this function. Next you'll strip out everything that has
nothing to do with replying with a status.
## Using `class API`
You create the `API` class using the Express `request` and `response`
objects in your `api/` handler, then use the methods `.reply()`, `.redirect()`,
or `.error()` to report status of the HTTP request. Let's strip down
your generated `api/running.js` file to the minimum necessary:
```javascript
import logging from '../lib/logging.js';
import { API } from '../lib/api.js';
const log = logging.create(import.meta.url);
export const get = async (req, res) => {
const api = new API(req, res);
try {
api.reply(200, { message: "OK"});
} catch (error) {
log.error(error);
api.error(500, error.message || "Internal Server Error");
}
}
```
Here you can see I am only leaving logging, `API`, and returning simple replies
with `api.reply(200, {message: "OK"))` or a more complex error reply if that fails.
## Authenticating Requests
Authentication for a handler is very simple and mostly done for you:
```javascript
// add this after the above get method
get.authenticated = true;
```
You "tag" any handlers you want protected with authentication by setting the `authenticated`
variable on them. The `commands/api.js` runner then knows to demand authentication before
running this handler. When it is run you can then use `API.user` to get at the authenticated
user.
## Form Validation
Validating forms in a server is very important, as you can't trust that any
browser actually performed the validation. The `API` class uses the wonderful
[Laravel validation rules](https://laravel.com/docs/9.x/validation) via the
very nice [Validation](https://www.npmjs.com/package/Validator) module by
[jfstn](https://github.com/jfstn/Validator). These rules are also the same
as returned by the `lib/ormish.js:Model.validation` method. That means you
can:
1. Get your Model from `lib/models.js`.
2. Call `TheModel.validation(rules)` with empty '' for any rules you want filled in based on the database schema.
3. Pass those rules to `API.validate()` to check inputs match the Model's rules.
4. Return a `API.validation_error()` when they fail.
Here's an example taken from the `api/register.js` handler, but stipped down to
the essentials so it only validates a user form then returns the cleaned up form:
```javascript
import { User, Site, } from "../lib/models.js";
const rules = User.validation({
email: '',
full_name: '',
initials: 'required|alpha|max:3',
password_repeat: 'required|same:password',
password: '',
tos_agree: 'required|boolean|accepted',
});
export const post = async (req, res) => {
const api = new API(req, res);
const form = api.validate(rules);
if(!form._valid) {
return api.validation_error(res, form);
} else {
api.clean_form(form, rules);
api.reply(200, {clean_user: form});
}
}
```
## A Good Validation Pattern
Validating forms and input is ver important, but you want to do it in a way that
doesn't interfere with a user's typing, yet still uses the client-side browser to
speed up the validation feedback. There's also the problem that _all_ form validation
systems seem to need the validation rules in both the backend and frontend of the
application.
I've found the best pattern to handle all of these is to "validate on first submit, return
the rules for later submits." It works like this:
1. On the first view of the form do _not_ provide any validation feedback. This removes the annoying noise of fields claiming inputs are wrong when they're simply being worked on. For example, as someone types their email it's annoying to flash it right/wrong until they're done.
2. Always validate based on the rules, and even better based on the database schema.
3. When this first validation fails, return a validation failure _and_ return the rules for the browser to use for later attempts.
4. The browser then has the rules, shows that there was an error, and can then provide immediate fast feedback to the user and block submits until its correct.
5. This also places the rules in only one place: the backend, where they're absolutely required.
## Client Side Validation
The validation process I described needs one more component on the client side, which is
demonstrated in the `client/pages/Registration.svelte` page:
```javascript
const register = async () => {
let [status, data] = await api.post("/api/register", form);
if(status === 200) {
$user.new_registration = true;
push('/');
} else {
form = Object.assign(form, data);
}
}
```
The `api.post("/api/register", form)` then uses `API.validation_error` to return an
error response which contains the errors _and_ validation rules that the browser
should use. Remember, the `api.post()` above is from `client/api.js:post` while
the `API.validation_error` is from this file.
___BUG___: Currently `client/pages/Registration.svelte` doesn't demonstrate the other part
of using `client/api.js:validate` to do more validations in the browser.
## Going Further
Once you get this working that's mostly all there is to writing an `api/` handler in
The Bandolier. You'll then want to learn how `lib/ormish.js` and `lib/models.js` works,
and how to create new UIs in `client/` or `admin/`. On the client side you'll want to
study the `client/api.js` which is the counterpart to this module, and look at the
`client/components/FormField.svelte` plus `client/pages/Registration.svelte` for other
examples.
*/
import Validator from 'Validator';
/*
Used internally to handle security decisions based on whether someone
runs the app with `DANGER_ADMIN=1` environment variable set or not.
Originally I was very loose in how this can be set, with sometimes `DANGER_ADMING=true`
or `DANGER_ADMIN="1"`, but I've since normalized that it will _only_ work if
`DANGER_ADMIN==="1"`.
*/
export const developer_admin = process.env.DANGER_ADMIN === "1";
export class API {
constructor(req, res) {
this.req = req;
this.res = res;
}
reply(status, data) {
return this.res.status(status).json(data);
}
redirect(url, status=301) {
return this.res.redirect(status, url);
}
error(status, err) {
if(typeof err === 'string') {
return this.res.status(status).send({message: err});
} else {
return this.res.status(status).send(err);
}
}
get user_authenticated() {
return this.req.user !== undefined;
}
get admin_authenticated() {
return this.req.user !== undefined && this.req.user.admin === 1;
}
get user() {
return this.req.user;
}
validate(rules, extra) {
const form = this.req.method === "GET" ? this.req.query : this.req.body;
let validation = Validator.make(form, rules);
// keep the rules as they're needed everywhere
form._rules = rules;
// BUG: validator has a bug that considers an empty
// form valid even if there are required rules
if(Object.getOwnPropertyNames(form).length === 0) {
// add in a fake entry in the form
form["_empty"] = null;
// validate it
form._valid = validation.passes();
// then remove _empty to keep it clean
delete form["_empty"];
} else {
form._valid = validation.passes();
}
form._errors = validation.getErrors();
if(extra) extra(form);
return form;
}
validation_error(res, form) {
return res.status(400).json(form);
}
clean_form(form, rules, extras=[]) {
// what I wouldn't give for set operations
for(let [key, rule] of Object.entries(form)) {
if(!(key in rules)) {
delete form[key];
}
}
for(let field of extras) {
delete form[field];
}
}
}
export const defer = () => {
let res;
let rej;
let promise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
promise.resolve = res;
promise.reject = rej;
return promise;
}