wip(backend): concluded apikeys routes and docs
This commit is contained in:
parent
d563bb2d17
commit
bd7c3e8b6e
9 changed files with 415 additions and 45 deletions
|
@ -12,38 +12,39 @@ refer to [the section on authenticating to the
|
||||||
API](/reference/backend/api#authentication).
|
API](/reference/backend/api#authentication).
|
||||||
</Tip>
|
</Tip>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Create a new API key
|
## Create a new API key
|
||||||
|
|
||||||
Create a new API key. The API key will belong to the user who is authenticated
|
Create a new API key. The API key will belong to the user who is authenticated
|
||||||
when making the call. Supported for both JWT and KEY authentication.
|
when making the call. Supported for both JWT and KEY authentication.
|
||||||
|
|
||||||
|
<Note compact>
|
||||||
|
The response to this API call is the only time the secret will be
|
||||||
|
revealed.
|
||||||
|
</Note>
|
||||||
|
|
||||||
### Endpoints
|
### Endpoints
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description | Auth |
|
||||||
| ------ | ---- | ----------- |
|
| ------ | ---- | ----------- | ---- |
|
||||||
| `POST` | `/apikey/jwt` | Create a new API key. Endpoint for JWT authentication |
|
| <Method post /> | `/apikey/jwt` | Create a new API key | _jwt_ |
|
||||||
| `POST` | `/apikey/key` | Create a new API key. Endpoint for API key authentication |
|
| <Method post /> | `/apikey/key` | Create a new API key | _key_ |
|
||||||
|
|
||||||
### Parameters
|
### Parameters
|
||||||
<Tabs tabs="Request, Response">
|
<Tabs tabs="Request, Response">
|
||||||
<Tab>
|
<Tab>
|
||||||
| Variable | Type | Description |
|
| Where | Variable | Type | Description |
|
||||||
| -------- | -------- | ----------- |
|
| ----- | ------------- | -------- | ----------- |
|
||||||
| `name` | `string` | Create a new API key. Endpoint for JWT authentication |
|
| _body_ | `name` | `string` | Create a new API key. Endpoint for JWT authentication |
|
||||||
| `level` | `number` | A privilege level from 0 to 8. |
|
| _body_ | `level` | `number` | A privilege level from 0 to 8. |
|
||||||
| `expiresIn` | `number` | The number of seconds until this key expires. |
|
| _body_ | `expiresIn` | `number` | body | The number of seconds until this key expires. |
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab>
|
<Tab>
|
||||||
Returns status code `200` on success, `400` on if the request is malformed, and
|
Returns HTTP status code <StatusCode status="201"/> on success, <StatusCode status="400"/> if
|
||||||
`500` on server error.
|
the request is malformed, and <StatusCode status="500"/> on server error.
|
||||||
|
|
||||||
| Value | Type | Description |
|
| Value | Type | Description |
|
||||||
| ------------------- | -------- | ----------- |
|
| ------------------- | -------- | ----------- |
|
||||||
| `result` | `string` | `success` on success, and `error` on error |
|
| `result` | `string` | `created` on success, and `error` on error |
|
||||||
| `apikey.key` | `string` | The API key |
|
| `apikey.key` | `string` | The API key |
|
||||||
| `apikey.secret` | `string` | The API secret |
|
| `apikey.secret` | `string` | The API secret |
|
||||||
| `apikey.level` | `number` | The privilege level of the API key |
|
| `apikey.level` | `number` | The privilege level of the API key |
|
||||||
|
@ -57,7 +58,7 @@ Returns status code `200` on success, `400` on if the request is malformed, and
|
||||||
<Tabs tabs="Request, Response">
|
<Tabs tabs="Request, Response">
|
||||||
<Tab>
|
<Tab>
|
||||||
```js
|
```js
|
||||||
const token = axios.post(
|
const apiKey = axios.post(
|
||||||
'https://backend.freesewing.org/apikey/jwt',
|
'https://backend.freesewing.org/apikey/jwt',
|
||||||
{
|
{
|
||||||
name: 'My first API key',
|
name: 'My first API key',
|
||||||
|
@ -90,6 +91,185 @@ const token = axios.post(
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
## Read an API key
|
||||||
|
|
||||||
|
Reads an existing API key. Note that the API secret can only be retrieved at
|
||||||
|
the moment the API key is created.
|
||||||
|
|
||||||
|
<Note compact>
|
||||||
|
You need the `admin` role to read API keys of other users
|
||||||
|
</Note>
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Description | Auth |
|
||||||
|
| ------ | ---- | ----------- | ---- |
|
||||||
|
| <Method get /> | `/apikey/:id/jwt` | Reads an API key | _jwt_ |
|
||||||
|
| <Method get /> | `/apikey/:id/key` | Reads an API key | _key_ |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
<Tabs tabs="Request, Response">
|
||||||
|
<Tab>
|
||||||
|
| Where | Variable | Type | Description |
|
||||||
|
| ----- | ----------- | -------- | ----------- |
|
||||||
|
| _url_ | `:id` | `string` | The `key` field of the API key |
|
||||||
|
</Tab>
|
||||||
|
<Tab>
|
||||||
|
Returns HTTP status code <StatusCode status="200"/> on success, <StatusCode status="400"/> if
|
||||||
|
the request is malformed, <StatusCode status="404"/> if the key is not found,
|
||||||
|
and <StatusCode status="500"/> on server error.
|
||||||
|
|
||||||
|
| Value | Type | Description |
|
||||||
|
| ------------------- | -------- | ----------- |
|
||||||
|
| `result` | `string` | `success` on success, and `error` on error |
|
||||||
|
| `apikey.key` | `string` | The API key |
|
||||||
|
| `apikey.level` | `number` | The privilege level of the API key |
|
||||||
|
| `apikey.expiresAt` | `string` | A string representation of the moment the API key expires |
|
||||||
|
| `apikey.name` | `string` | The name of the API key |
|
||||||
|
| `apikey.userId` | `number` | The ID of the user who created the API key |
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
### Example
|
||||||
|
<Tabs tabs="Request, Response">
|
||||||
|
<Tab>
|
||||||
|
```js
|
||||||
|
const keyInfo = axios.get(
|
||||||
|
'https://backend.freesewing.org/apikey/7ea12968-7758-40b6-8c73-75cc99be762b/jwt',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
result: 'success',
|
||||||
|
apikey: {
|
||||||
|
key: '7ea12968-7758-40b6-8c73-75cc99be762b',
|
||||||
|
level: 3,
|
||||||
|
expiresAt: '2022-11-06T15:57:30.190Z',
|
||||||
|
name: 'My first API key',
|
||||||
|
userId: 61
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Read the current API key
|
||||||
|
|
||||||
|
Reads the API key with which the current request was authenticated.
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Description | Auth |
|
||||||
|
| ------ | ---- | ----------- | ---- |
|
||||||
|
| <Method get /> | `/whoami/key` | Reads the current API key | _key_ |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
<Tabs tabs="Request, Response">
|
||||||
|
<Tab>
|
||||||
|
<Note compact>This endpoint takes no parameters</Note>
|
||||||
|
</Tab>
|
||||||
|
<Tab>
|
||||||
|
Returns status code `200` on success, `400` on if the request is malformed,
|
||||||
|
`404` if the key is not found, and `500` on server error.
|
||||||
|
|
||||||
|
| Value | Type | Description |
|
||||||
|
| ------------------- | -------- | ----------- |
|
||||||
|
| `result` | `string` | `success` on success, and `error` on error |
|
||||||
|
| `apikey.key` | `string` | The API key |
|
||||||
|
| `apikey.level` | `number` | The privilege level of the API key |
|
||||||
|
| `apikey.expiresAt` | `string` | A string representation of the moment the API key expires |
|
||||||
|
| `apikey.name` | `string` | The name of the API key |
|
||||||
|
| `apikey.userId` | `number` | The ID of the user who created the API key |
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
### Example
|
||||||
|
<Tabs tabs="Request, Response">
|
||||||
|
<Tab>
|
||||||
|
```js
|
||||||
|
const keyInfo = axios.get(
|
||||||
|
'https://backend.freesewing.org/whoami/key',
|
||||||
|
{
|
||||||
|
auth: {
|
||||||
|
username: apikey.key,
|
||||||
|
password: apikey.secret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
result: 'success',
|
||||||
|
apikey: {
|
||||||
|
key: '7ea12968-7758-40b6-8c73-75cc99be762b',
|
||||||
|
level: 3,
|
||||||
|
expiresAt: '2022-11-06T15:57:30.190Z',
|
||||||
|
name: 'My first API key',
|
||||||
|
userId: 61
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
|
||||||
|
## Remove an API key
|
||||||
|
|
||||||
|
Removes an existing API key.
|
||||||
|
|
||||||
|
<Note compact>
|
||||||
|
You need the `admin` role to remove API keys of other users
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Description | Auth |
|
||||||
|
| ------ | ---- | ----------- | ---- |
|
||||||
|
| <Method delete /> | `/apikey/:id/jwt` | Removes an API key | _jwt_ |
|
||||||
|
| <Method delete /> | `/apikey/:id/key` | Removes an API key | _key_ |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
<Tabs tabs="Request, Response">
|
||||||
|
<Tab>
|
||||||
|
| Where | Variable | Type | Description |
|
||||||
|
| ----- | ----------- | -------- | ----------- |
|
||||||
|
| _url_ | `:id` | `string` | The `key` field of the API key |
|
||||||
|
</Tab>
|
||||||
|
<Tab>
|
||||||
|
Returns HTTP status code <StatusCode status="204"/> on success, <StatusCode status="400"/> if
|
||||||
|
the request is malformed, <StatusCode status="404"/> if the key is not found,
|
||||||
|
and <StatusCode status="500"/> on server error.
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
### Example
|
||||||
|
<Tabs tabs="Request, Response">
|
||||||
|
<Tab>
|
||||||
|
```js
|
||||||
|
const keyInfo = axios.get(
|
||||||
|
'https://backend.freesewing.org/apikey/7ea12968-7758-40b6-8c73-75cc99be762b/jwt',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab>
|
||||||
|
<Note compact>Status code <StatusCode status="204"/> (no content) does not come with a body</Note>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
The following is good to keep in mind when working with API keys:
|
The following is good to keep in mind when working with API keys:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
---
|
---
|
||||||
title: REST API
|
title: Backend REST API
|
||||||
|
linktitle: REST API
|
||||||
---
|
---
|
||||||
|
|
||||||
This is the reference documentation for the FreeSewing backend REST API.
|
This is the reference documentation for the FreeSewing backend REST API.
|
||||||
|
@ -19,30 +20,35 @@ automation tasks such as creating issues on Github.
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
This API is not accessible without authentication.
|
Apart from a handlful of API endpoints that are accessible without
|
||||||
|
authentication (typically the ones dealing with the signup flow or password
|
||||||
|
recovery), this API requires authentication.
|
||||||
|
|
||||||
The FreeSewing backend API allows two types of authentication:
|
Two different types of authentication are supports:
|
||||||
|
|
||||||
- JSON Web Tokens (jwt): This is typically used to authenticate humans in a
|
- **JSON Web Tokens** (jwt): This is typically used to authenticate humans in a
|
||||||
browser session.
|
browser session.
|
||||||
- API Keys (key): This is typically used to interact with the API in an
|
- **API Keys** (key): This is typically used to interact with the API in an
|
||||||
automated way. Like in a script, or a CI/CD context.
|
automated way. Like in a script, a CI/CD context, a serverless runner, and so
|
||||||
|
on.
|
||||||
|
|
||||||
Apart from the handlful of API endpoints that are accessible without
|
For each endpoint, the API has a variant depending on what authentication you
|
||||||
authentication (typically the ones dealing with the signup flow), this API has
|
want to use:
|
||||||
a variant for each route depending on what authentication you want to use:
|
|
||||||
|
|
||||||
- `/some/route/jwt` : Authenticate with JWT
|
- `/some/route/jwt` : Authenticate with JWT
|
||||||
- `/some/route/key` : Authenticate with an API key and secret
|
- `/some/route/key` : Authenticate with an API key and secret
|
||||||
|
|
||||||
### JWT authentication
|
### JWT authentication
|
||||||
|
|
||||||
The use of JSON Web Tokens ([jwt](https://jwt.io)) is typically used in a browser context where we want to establish a *session*.
|
The use of JSON Web Tokens ([jwt](https://jwt.io)) is typically used in a
|
||||||
|
browser context where we want to establish a *session*.
|
||||||
|
|
||||||
To get a token, you must first authenticate at the `/login` endpoint with username and password.
|
To get a token, you must first authenticate at the `/login` endpoint with
|
||||||
You will receive a token in the response.
|
username and password. You will receive a JSON Web Token (jwt) as part of the
|
||||||
|
response.
|
||||||
|
|
||||||
In subsequent API calls, you must then include this token in the `Authorization` header prefixed by `Bearer`. Liek his:
|
In subsequent API calls, you must then include this token in the
|
||||||
|
`Authorization` header prefixed by `Bearer`. Like his:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const account = await axios.get(
|
const account = await axios.get(
|
||||||
|
@ -57,11 +63,18 @@ const account = await axios.get(
|
||||||
|
|
||||||
### API key authentication
|
### API key authentication
|
||||||
|
|
||||||
The combination API key & secret serves as a username & password for HTTP basic authentication.
|
The combination of API key & secret serves as a username & password for [HTTP
|
||||||
In basic authentication, the password is sent unencrypted, but since the FreeSewing backend is only reachable over a TLS encrypted connection, this is not a problem.
|
basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).
|
||||||
|
|
||||||
On the plus side, sending a username and password with a request is supported pretty much everywhere.
|
<Note>
|
||||||
In addition, there is no need to establish a session first, so this make the entire transation stateless.
|
In basic authentication, the password is sent
|
||||||
|
unencrypted. To guard against this, this API should only be served over a
|
||||||
|
connectin encrypted with TLS. (a url starting with `https://`).
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
Sending a username and password with a request like this is supported
|
||||||
|
pretty much everywhere. In addition, there is no need to establish a session
|
||||||
|
first, so this make the entire transation stateless.
|
||||||
|
|
||||||
Below is an example using curl:
|
Below is an example using curl:
|
||||||
|
|
||||||
|
@ -96,8 +109,8 @@ The table below lists the priviledge of all levels as well as their correspondin
|
||||||
| `4` | **write all** account data | ✅ | ✅ | ✅ | ✅ |
|
| `4` | **write all** account data | ✅ | ✅ | ✅ | ✅ |
|
||||||
| `5` | **read** measurements or patterns of **other users** | ❌ | ✅ | ✅ | ✅ |
|
| `5` | **read** measurements or patterns of **other users** | ❌ | ✅ | ✅ | ✅ |
|
||||||
| `6` | **read all** account data of **other users** | ❌ | ❌ | ✅ | ✅ |
|
| `6` | **read all** account data of **other users** | ❌ | ❌ | ✅ | ✅ |
|
||||||
| `7` | **write** access through **specific support methods** | ❌ | ❌ | ✅ | ✅ |
|
| `7` | **write** account data of **other users** through **specific support methods** | ❌ | ❌ | ✅ | ✅ |
|
||||||
| `8` | impersonate other user, full write access | ❌ | ❌ | ❌ | ✅ |
|
| `8` | impersonate other users, **full write access** | ❌ | ❌ | ❌ | ✅ |
|
||||||
|
|
||||||
## API Routes
|
## API Routes
|
||||||
|
|
||||||
|
|
|
@ -2,4 +2,57 @@
|
||||||
title: FreeSewing backend
|
title: FreeSewing backend
|
||||||
---
|
---
|
||||||
|
|
||||||
FIXME: Explain what this is about
|
The FreeSewing backend handles all user data. Prior to version 3 of FreeSewing,
|
||||||
|
the backend was only used internally as the data store for our frontend, the
|
||||||
|
FreeSewing.org website.
|
||||||
|
|
||||||
|
In version 3, we have rewritten the backend with the explicit goal to offer it
|
||||||
|
as a service to users and developers. This allows integration with other tools
|
||||||
|
such as hosted instances of our lab, CLI tools, serverless runners, CI/CD
|
||||||
|
environments and so on.
|
||||||
|
|
||||||
|
In other words, we no longer merely provide our own frontend, you can now also
|
||||||
|
use our backend as a service to build your own projects.
|
||||||
|
|
||||||
|
## Changes for developers
|
||||||
|
|
||||||
|
### Authentication with JWT and API keys
|
||||||
|
|
||||||
|
Before version 3, the backend only supported authentication via JSON Web
|
||||||
|
Tokens. That's fine for a browser session, but not very handy if you want to
|
||||||
|
talk to the API directly.
|
||||||
|
|
||||||
|
Since version 3, we support authentication with API keys. Furthermore, we
|
||||||
|
allow any FreeSewing user to generate their own API keys.
|
||||||
|
|
||||||
|
In other words, if you want to connect to our backend API, you don't need to
|
||||||
|
ask us. You can generate your own API key and start right away.
|
||||||
|
|
||||||
|
We've made a number of changes to make it easier for external developers and
|
||||||
|
contributors to work with our backend.
|
||||||
|
|
||||||
|
### Sqlite instead of MongoDB
|
||||||
|
|
||||||
|
Our backend used to use MongoDB for storage. Since version 3, we've moved to
|
||||||
|
Sqlite which is a file-based database making local development a breeze since
|
||||||
|
you don't need to run a local database server.
|
||||||
|
|
||||||
|
### Sanity instead of local storage
|
||||||
|
|
||||||
|
We now use Sanity and the Sanity API to stored images for users (avatars for
|
||||||
|
user accounts and people). Furthermore, we also generate PDFs in the browser
|
||||||
|
now so we also don't need storage for that.
|
||||||
|
|
||||||
|
As a result, our backend does not need any storage, only access to the Sqlite
|
||||||
|
file. This also makes it easier to work with the backend as a developer.
|
||||||
|
|
||||||
|
## Use, don't abuse
|
||||||
|
|
||||||
|
Our backend API runs in a cloud environment and while we do not charge for
|
||||||
|
access to the API, we do need to pay the bills of said cloud provider.
|
||||||
|
|
||||||
|
As such, please be mindful of the amount of requests you generate. And if you
|
||||||
|
have big plans, please reach out to us to discuss them first.
|
||||||
|
|
||||||
|
We will monitor the use of our backend API and we may at any moment decide to
|
||||||
|
revoke API keys if we feel the use is beyond what we can or want to support.
|
||||||
|
|
|
@ -62,3 +62,16 @@ ApikeyController.prototype.whoami = async (req, res, tools) => {
|
||||||
|
|
||||||
return Apikey.sendResponse(res)
|
return Apikey.sendResponse(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Remove API key
|
||||||
|
*
|
||||||
|
* This is the endpoint that handles removal of API keys/tokens
|
||||||
|
* See: https://freesewing.dev/reference/backend/api/apikey
|
||||||
|
*/
|
||||||
|
ApikeyController.prototype.delete = async (req, res, tools) => {
|
||||||
|
const Apikey = new ApikeyModel(tools)
|
||||||
|
await Apikey.removeIfAllowed({ id: req.params.id }, req.user)
|
||||||
|
|
||||||
|
return Apikey.sendResponse(res)
|
||||||
|
}
|
||||||
|
|
|
@ -25,7 +25,8 @@ ApikeyModel.prototype.setResponse = function (status = 200, error = false, data
|
||||||
...data,
|
...data,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if (status > 201) {
|
if (status === 201) this.response.body.result = 'created'
|
||||||
|
else if (status > 201) {
|
||||||
this.response.body.error = error
|
this.response.body.error = error
|
||||||
this.response.body.result = 'error'
|
this.response.body.result = 'error'
|
||||||
this.error = true
|
this.error = true
|
||||||
|
@ -68,12 +69,34 @@ ApikeyModel.prototype.readIfAllowed = async function (where, user) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApikeyModel.prototype.removeIfAllowed = async function (where, user) {
|
||||||
|
if (!this.User.authenticatedUser) await this.User.loadAuthenticatedUser(user)
|
||||||
|
await this.read(where)
|
||||||
|
if (!this.record) return this.setResponse(404, 'apikeyNotFound')
|
||||||
|
if (this.record.userId !== this.User.authenticatedUser.id) {
|
||||||
|
// Not own key - only admin can do that
|
||||||
|
if (this.User.authenticatedUser.role !== 'admin') {
|
||||||
|
return this.setResponse(400, 'permissionLackingToRemoveOtherApiKey')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.remove(where)
|
||||||
|
|
||||||
|
return this.setResponse(204)
|
||||||
|
}
|
||||||
|
|
||||||
ApikeyModel.prototype.read = async function (where) {
|
ApikeyModel.prototype.read = async function (where) {
|
||||||
this.record = await this.prisma.apikey.findUnique({ where })
|
this.record = await this.prisma.apikey.findUnique({ where })
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApikeyModel.prototype.remove = async function (where) {
|
||||||
|
await this.prisma.apikey.delete({ where })
|
||||||
|
this.record = false
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
ApikeyModel.prototype.create = async function ({ body, user }) {
|
ApikeyModel.prototype.create = async function ({ body, user }) {
|
||||||
if (Object.keys(body) < 1) return this.setResponse(400, 'postBodyMissing')
|
if (Object.keys(body) < 1) return this.setResponse(400, 'postBodyMissing')
|
||||||
if (!body.name) return this.setResponse(400, 'nameMissing')
|
if (!body.name) return this.setResponse(400, 'nameMissing')
|
||||||
|
@ -101,7 +124,7 @@ ApikeyModel.prototype.create = async function ({ body, user }) {
|
||||||
name: body.name,
|
name: body.name,
|
||||||
level: body.level,
|
level: body.level,
|
||||||
secret: asJson(hashPassword(secret)),
|
secret: asJson(hashPassword(secret)),
|
||||||
userId: user._id,
|
userId: user._id || user.userId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -109,7 +132,7 @@ ApikeyModel.prototype.create = async function ({ body, user }) {
|
||||||
return this.setResponse(500, 'createApikeyFailed')
|
return this.setResponse(500, 'createApikeyFailed')
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.setResponse(200, 'success', {
|
return this.setResponse(201, 'created', {
|
||||||
apikey: {
|
apikey: {
|
||||||
key: this.record.id,
|
key: this.record.id,
|
||||||
secret,
|
secret,
|
||||||
|
|
|
@ -27,4 +27,12 @@ export function apikeyRoutes(tools) {
|
||||||
app.get('/whoami/key', passport.authenticate(...bsc), (req, res) =>
|
app.get('/whoami/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
Apikey.whoami(req, res, tools)
|
Apikey.whoami(req, res, tools)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Remove Apikey
|
||||||
|
app.delete('/apikey/:id/jwt', passport.authenticate(...jwt), (req, res) =>
|
||||||
|
Apikey.delete(req, res, tools)
|
||||||
|
)
|
||||||
|
app.delete('/apikey/:id/key', passport.authenticate(...bsc), (req, res) =>
|
||||||
|
Apikey.delete(req, res, tools)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -287,10 +287,10 @@ describe(`${user} Signup flow and authentication`, () => {
|
||||||
expiresIn: 60,
|
expiresIn: 60,
|
||||||
})
|
})
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.status).to.equal(200)
|
expect(res.status).to.equal(201)
|
||||||
expect(res.type).to.equal('application/json')
|
expect(res.type).to.equal('application/json')
|
||||||
expect(res.charset).to.equal('utf-8')
|
expect(res.charset).to.equal('utf-8')
|
||||||
expect(res.body.result).to.equal(`success`)
|
expect(res.body.result).to.equal(`created`)
|
||||||
expect(typeof res.body.apikey.key).to.equal('string')
|
expect(typeof res.body.apikey.key).to.equal('string')
|
||||||
expect(typeof res.body.apikey.secret).to.equal('string')
|
expect(typeof res.body.apikey.secret).to.equal('string')
|
||||||
expect(typeof res.body.apikey.expiresAt).to.equal('string')
|
expect(typeof res.body.apikey.expiresAt).to.equal('string')
|
||||||
|
@ -300,6 +300,30 @@ describe(`${user} Signup flow and authentication`, () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
step(`${user} Create API Key with KEY`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.post('/apikey/key')
|
||||||
|
.auth(store.apikey.key, store.apikey.secret)
|
||||||
|
.send({
|
||||||
|
name: 'Test API key with key',
|
||||||
|
level: 4,
|
||||||
|
expiresIn: 60,
|
||||||
|
})
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(res.status).to.equal(201)
|
||||||
|
expect(res.type).to.equal('application/json')
|
||||||
|
expect(res.charset).to.equal('utf-8')
|
||||||
|
expect(res.body.result).to.equal(`created`)
|
||||||
|
expect(typeof res.body.apikey.key).to.equal('string')
|
||||||
|
expect(typeof res.body.apikey.secret).to.equal('string')
|
||||||
|
expect(typeof res.body.apikey.expiresAt).to.equal('string')
|
||||||
|
expect(res.body.apikey.level).to.equal(4)
|
||||||
|
store.apikeykey = res.body.apikey
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
step(`${user} Read API Key with KEY (whoami)`, (done) => {
|
step(`${user} Read API Key with KEY (whoami)`, (done) => {
|
||||||
chai
|
chai
|
||||||
.request(config.api)
|
.request(config.api)
|
||||||
|
@ -320,7 +344,7 @@ describe(`${user} Signup flow and authentication`, () => {
|
||||||
chai
|
chai
|
||||||
.request(config.api)
|
.request(config.api)
|
||||||
.get(`/apikey/${store.apikey.key}/key`)
|
.get(`/apikey/${store.apikey.key}/key`)
|
||||||
.auth(store.apikey.key, store.apikey.secret)
|
.auth(store.apikeykey.key, store.apikeykey.secret)
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.status).to.equal(200)
|
expect(res.status).to.equal(200)
|
||||||
expect(res.type).to.equal('application/json')
|
expect(res.type).to.equal('application/json')
|
||||||
|
@ -335,7 +359,7 @@ describe(`${user} Signup flow and authentication`, () => {
|
||||||
step(`${user} Read API Key with JWT`, (done) => {
|
step(`${user} Read API Key with JWT`, (done) => {
|
||||||
chai
|
chai
|
||||||
.request(config.api)
|
.request(config.api)
|
||||||
.get(`/apikey/${store.apikey.key}/jwt`)
|
.get(`/apikey/${store.apikeykey.key}/jwt`)
|
||||||
.set('Authorization', 'Bearer ' + store.token)
|
.set('Authorization', 'Bearer ' + store.token)
|
||||||
.end((err, res) => {
|
.end((err, res) => {
|
||||||
expect(res.status).to.equal(200)
|
expect(res.status).to.equal(200)
|
||||||
|
@ -343,7 +367,18 @@ describe(`${user} Signup flow and authentication`, () => {
|
||||||
expect(res.charset).to.equal('utf-8')
|
expect(res.charset).to.equal('utf-8')
|
||||||
expect(res.body.result).to.equal(`success`)
|
expect(res.body.result).to.equal(`success`)
|
||||||
const checks = ['key', 'level', 'expiresAt', 'name', 'userId']
|
const checks = ['key', 'level', 'expiresAt', 'name', 'userId']
|
||||||
checks.forEach((i) => expect(res.body.apikey[i]).to.equal(store.apikey[i]))
|
checks.forEach((i) => expect(res.body.apikey[i]).to.equal(store.apikeykey[i]))
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
step(`${user} Remove API Key with KEY`, (done) => {
|
||||||
|
chai
|
||||||
|
.request(config.api)
|
||||||
|
.delete(`/apikey/${store.apikeykey.key}/key`)
|
||||||
|
.auth(store.apikeykey.key, store.apikeykey.secret)
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(res.status).to.equal(204)
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,8 +7,52 @@ import { Tab, Tabs } from './tabs.js'
|
||||||
import Example from './example.js'
|
import Example from './example.js'
|
||||||
import Examples from './examples.js'
|
import Examples from './examples.js'
|
||||||
|
|
||||||
|
const methodClasses = {
|
||||||
|
get: 'bg-green-600 text-white',
|
||||||
|
post: 'bg-sky-600 text-white',
|
||||||
|
put: 'bg-orange-500 text-white',
|
||||||
|
delete: 'bg-red-600 text-white',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Method = (props) => {
|
||||||
|
let method = false
|
||||||
|
for (const m in methodClasses) {
|
||||||
|
if (!method && props[m]) method = m.toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`my-1 text-xs inline-flex items-center font-bold leading-sm uppercase px-3 py-1 rounded-full ${
|
||||||
|
methodClasses[method.toLowerCase()]
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{method}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusClasses = {
|
||||||
|
2: 'bg-green-600 text-white',
|
||||||
|
4: 'bg-orange-500 text-white',
|
||||||
|
5: 'bg-red-600 text-white',
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusCode = ({ status }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`my-1 text-xs inline-flex items-center font-bold leading-sm uppercase px-3 py-1 rounded-full ${
|
||||||
|
statusClasses['' + status.slice(0, 1)]
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const mdxCustomComponents = (app = false) => ({
|
const mdxCustomComponents = (app = false) => ({
|
||||||
// Custom components
|
// Custom components
|
||||||
|
Method,
|
||||||
|
StatusCode,
|
||||||
Comment: (props) => <Popout {...props} comment />,
|
Comment: (props) => <Popout {...props} comment />,
|
||||||
Fixme: (props) => <Popout {...props} fixme />,
|
Fixme: (props) => <Popout {...props} fixme />,
|
||||||
Link: (props) => <Popout {...props} link />,
|
Link: (props) => <Popout {...props} link />,
|
||||||
|
|
|
@ -18,3 +18,4 @@
|
||||||
<!-- Background opacity for highlighted lines in code -->
|
<!-- Background opacity for highlighted lines in code -->
|
||||||
<code class="bg-yellow-300 bg-opacity-5" />
|
<code class="bg-yellow-300 bg-opacity-5" />
|
||||||
<code class="bg-orange-300 bg-opacity-5 opacity-80 line-through decoration-orange-500" />
|
<code class="bg-orange-300 bg-opacity-5 opacity-80 line-through decoration-orange-500" />
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue