Files
directus/docs/guides/api-hooks.md
Aiden Foxx fa3b1171e8 New OpenID and OAuth2 drivers (#8660)
* Moved over oauth impl to new interface

* Fixed most build issues and started addind schema to auth drivers

* Finished up OAuth2 and OpenID drivers

* Removed unused migration and utils

* Fixed minor todos

* Removed old oauth flow

* Changed oauth flow to re-use refresh token

* Added new oauth frontend

* Added font awesome social icons

* Updated authentication documentation

* Update api/src/auth/drivers/oauth2.ts

* Tested implementation and fixed incorrect validation

* Updated docs

* Improved OAuth error handling and re-enabled creating users with provider/identifier

* Removed Session config from docs

* Update app/src/components/v-icon/v-icon.vue

* Removed oauth need to define default roleID

* Added FormatTitle to SSO links

* Prevent local auth without password

* Store OAuth access token in session data

* Update docs/guides/api-config.md

* Fixed copy and removed fontawesome-vue dependency

* More docs fixes

* Crucialy importend type fiks

* Update package-lock

* Remove is-email-allowed check

In favor of more advanced version based on filtering coming later

* Fix JSON type casting

* Delete unused util

* Update type signature to include name

* Add warning when code isn't found in oauth url

and remove obsolete imports

* Auto-continue on successful SSO login

* Tweak type signature

* More type casting shenanigans

* Please the TS gods

* Check for missing token before crashing

Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
2021-10-21 17:45:01 -04:00

259 lines
9.8 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Custom API Hooks <small></small>
> Custom API Hooks allow running custom logic when a specified event occurs within your project. They can be registered
> as either "blocking" or immediate.
## 1. Create a Hook File
Custom hooks are dynamically loaded from within your extensions folder. By default this directory is located at
`/extensions`, but it can be configured within your project's env file to be located anywhere.
### Default Standalone Hook Location
```
/extensions/hooks/<hook-id>/index.js
```
## 2. Define the Event
Next, you will want to define your event. You can trigger your custom hook with any of the platform's many API events.
System events are referenced with the format:
```
<scope>.<action>(.<before>)
// eg: items.create
// eg: files.upload
// eg: collections.*
// eg: users.update.before
```
### Scope
The scope determines the API endpoint that is triggered. The `*` wildcard can also be used to include all scopes.
::: tip System Scope
Currently all system tables are available as event scopes except for `directus_migrations` and `directus_sessions`,
which don't have relevant endpoints or services.
:::
### Action
Defines the triggering operation within the specified context (see chart below). The `*` wildcard can also be used to
include all actions available to the scope.
### Before
Many scopes (see chart below) support an optional `.before` suffix for running a _blocking_ hook prior to the event
being fired. This allows you to check and/or modify the event's payload before it is processed.
- `items.create.before` (Blocking)
- `items.create` (Non Blocking, also called 'after' implicitly)
This also allows you to cancel an event based on the logic within the hook. Below is an example of how you can cancel a
create event by throwing a standard Directus exception.
```js
module.exports = function registerHook({ exceptions }) {
const { InvalidPayloadException } = exceptions;
return {
'items.create.before': async function (input) {
if (LOGIC_TO_CANCEL_EVENT) {
throw new InvalidPayloadException(WHAT_IS_WRONG);
}
return input;
},
};
};
```
### Event Format Options
| Scope | Actions | Before |
| -------------------- | ------------------------------------------------------------------ | ---------------- |
| `cron()` | [See below for configuration](#interval-cron) | No |
| `cli.init` | `before` and `after` | No |
| `server` | `start` and `stop` | Optional |
| `init` | | Optional |
| `routes.init` | `before` and `after` | No |
| `routes.custom.init` | `before` and `after` | No |
| `middlewares.init` | `before` and `after` | No |
| `request` | `not_found` | No |
| `response` | | No<sup>[1]</sup> |
| `database.error` | When a database error is thrown | No |
| `error` | | No |
| `auth` | `login`, `logout`<sup>[1]</sup>, `jwt` and `refresh`<sup>[1]</sup> | Optional |
| `items` | `read`<sup>[2]</sup>, `create`, `update` and `delete` | Optional |
| `activity` | `create`, `update` and `delete` | Optional |
| `collections` | `create`, `update` and `delete` | Optional |
| `fields` | `create`, `update` and `delete` | Optional |
| `files` | `upload`<sup>[2]</sup> | No |
| `folders` | `create`, `update` and `delete` | Optional |
| `permissions` | `create`, `update` and `delete` | Optional |
| `presets` | `create`, `update` and `delete` | Optional |
| `relations` | `create`, `update` and `delete` | Optional |
| `revisions` | `create`, `update` and `delete` | Optional |
| `roles` | `create`, `update` and `delete` | Optional |
| `settings` | `create`, `update` and `delete` | Optional |
| `users` | `create`, `update` and `delete` | Optional |
| `webhooks` | `create`, `update` and `delete` | Optional |
<sup>1</sup> Feature Coming Soon\
<sup>2</sup> Doesn't support `.before` modifier
#### Interval (cron)
Hooks support running on an interval through [`node-cron`](https://www.npmjs.com/package/node-cron). To set this up,
provide a cron statement in the event scope as follows: `cron(<statement>)`, for example `cron(15 14 1 * *)` (at 14:15
on day-of-month 1) or `cron(5 4 * * sun)` (at 04:05 on Sunday). See example below:
```js
const axios = require('axios');
module.exports = function registerHook() {
return {
'cron(*/15 * * * *)': async function () {
await axios.post('http://example.com/webhook', { message: 'Another 15 minutes passed...' });
},
};
};
```
## 3. Register your Hook
Each custom hook is registered to its event scope using a function with the following format:
```js
const axios = require('axios');
module.exports = function registerHook() {
return {
'items.create': function () {
axios.post('http://example.com/webhook');
},
};
};
```
## 4. Develop your Custom Hook
> Hooks can impact performance when not carefully implemented. This is especially true for `before` hooks (as these are
> blocking) and hooks on `read` actions, as a single request can result in a large amount of database reads.
### Register Function
The register function (eg: `module.exports = function registerHook()`) must return an object where the key is the event,
and the value is the handler function itself.
The `registerHook` function receives a context parameter with the following properties:
- `services` — All API internal services
- `exceptions` API exception objects that can be used for throwing "proper" errors
- `database` — Knex instance that is connected to the current database
- `getSchema` — Async function that reads the full available schema for use in services
- `env` Parsed environment variables
- `logger` — [Pino](https://github.com/pinojs/pino) instance.
### Event Handler Function
The event handler function (eg: `'items.create': function()`) receives a context parameter with the following
properties:
- `event` — Full event string
- `accountability` — Information about the current user
- `collection` — Collection that is being modified
- `item` Primary key(s) of the item(s) being modified
- `action` — Action that is performed
- `payload` Payload of the request
- `schema` - The current API schema in use
- `database` - Current database transaction
::: tip Input
The `items.*.before` hooks get the raw input payload as the first parameter, with the context parameter as the second
parameter.
:::
#### Items read
In contrast to the other `items` events (`items.create`, `items.update`, `items.delete`) the `items.read` doesn't
receive the primary key(s) of the items but the query used:
- `event` — Full event string
- `accountability` — Information about the current user
- `collection` — Collection that is being modified
- `query` The query used to get the data
- `action` — Action that is performed
- `payload` Payload of the request
- `schema` - The current API schema in use
- `database` - Current database transaction
#### Auth
The `auth` hooks have the following context properties:
- `event` — Full event string
- `accountability` — Information about the current user
- `action` — Action that is performed
- `payload` Payload of the request
- `schema` - The current API schema in use
- `status` - One of `pending`, `success`, `fail`
- `user` - ID of the user that tried logging in/has logged in
## 5. Restart the API
To deploy your hook, simply restart the API by running:
```bash
npx directus start
```
## Full Example
`extensions/hooks/sync-with-external/index.js`:
```js
const axios = require('axios');
module.exports = function registerHook({ services, exceptions }) {
const { MailService } = services;
const { ServiceUnavailableException, ForbiddenException } = exceptions;
return {
// Force everything to be admin-only at all times
'items.*': async function ({ item, accountability }) {
if (accountability.admin !== true) throw new ForbiddenException();
},
// Sync with external recipes service, cancel creation on failure
'items.create.before': async function (input, { collection, schema }) {
if (collection !== 'recipes') return input;
const mailService = new MailService({ schema });
try {
await axios.post('https://example.com/recipes', input);
await mailService.send({
to: 'person@example.com',
template: {
name: 'item-created',
data: {
collection: collection,
},
},
});
} catch (error) {
throw new ServiceUnavailableException(error);
}
input[0].syncedWithExample = true;
return input;
},
};
};
```