wip(backend): more work on routes and docs
This commit is contained in:
parent
3fc08d8bdb
commit
d563bb2d17
10 changed files with 349 additions and 17 deletions
134
markdown/dev/reference/backend/api/apikey/en.md
Normal file
134
markdown/dev/reference/backend/api/apikey/en.md
Normal file
|
@ -0,0 +1,134 @@
|
|||
---
|
||||
title: API Keys
|
||||
---
|
||||
|
||||
Documentation for the REST API endpoints to create, read, or remove API keys.
|
||||
|
||||
<Tip>
|
||||
The FreeSewing backend REST API supports authentication both with JSON Web
|
||||
Tokens (JWT) as with API keys (KEY). This describes the endpoints that deal
|
||||
with creating, reading, and removing API keys. For authentication details,
|
||||
refer to [the section on authenticating to the
|
||||
API](/reference/backend/api#authentication).
|
||||
</Tip>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Create a new API key
|
||||
|
||||
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.
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| `POST` | `/apikey/jwt` | Create a new API key. Endpoint for JWT authentication |
|
||||
| `POST` | `/apikey/key` | Create a new API key. Endpoint for API key authentication |
|
||||
|
||||
### Parameters
|
||||
<Tabs tabs="Request, Response">
|
||||
<Tab>
|
||||
| Variable | Type | Description |
|
||||
| -------- | -------- | ----------- |
|
||||
| `name` | `string` | Create a new API key. Endpoint for JWT authentication |
|
||||
| `level` | `number` | A privilege level from 0 to 8. |
|
||||
| `expiresIn` | `number` | The number of seconds until this key expires. |
|
||||
</Tab>
|
||||
<Tab>
|
||||
Returns status code `200` on success, `400` on if the request is malformed, and
|
||||
`500` on server error.
|
||||
|
||||
| Value | Type | Description |
|
||||
| ------------------- | -------- | ----------- |
|
||||
| `result` | `string` | `success` on success, and `error` on error |
|
||||
| `apikey.key` | `string` | The API key |
|
||||
| `apikey.secret` | `string` | The API secret |
|
||||
| `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 token = axios.post(
|
||||
'https://backend.freesewing.org/apikey/jwt',
|
||||
{
|
||||
name: 'My first API key',
|
||||
level: 2, // Read only
|
||||
expiresIn: 3600, // One hour
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
</Tab>
|
||||
<Tab>
|
||||
|
||||
```json
|
||||
{
|
||||
result: 'success',
|
||||
apikey: {
|
||||
key: '7ea12968-7758-40b6-8c73-75cc99be762b',
|
||||
secret: '503d7adbdb3ec18ab27adfcd895d8b47a8d6bc8307d548500fbf9c05a5a8820e',
|
||||
level: 3,
|
||||
expiresAt: '2022-11-06T15:57:30.190Z',
|
||||
name: 'My first API key',
|
||||
userId: 61
|
||||
}
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Notes
|
||||
|
||||
The following is good to keep in mind when working with API keys:
|
||||
|
||||
### This is not the authentication documentation
|
||||
|
||||
The FreeSewing backend REST API supports authentication both with JSON Web
|
||||
Tokens (JWT) as with API keys (KEY).
|
||||
|
||||
This describes the endpoints that deal with creating, reading, and removing API
|
||||
keys. For authentication details, refer to [the section on authenticating to
|
||||
the API](/reference/backend/api#authentication).
|
||||
|
||||
### API keys are immutable
|
||||
|
||||
Once created, API keys cannot be updated.
|
||||
You should remove them and re-create a new one if you want to make change.
|
||||
|
||||
### API keys have an expiry
|
||||
|
||||
API keys have an expiry date. The maximum validity for an API key is 1 year.
|
||||
|
||||
### API keys have a permission level
|
||||
|
||||
API keys have a permission level. You can never create an API key with a higher
|
||||
permission level than your own permission level.
|
||||
|
||||
### Circumstances that will trigger your API keys to be revoked
|
||||
|
||||
As a precaution, all your API keys will be revoked when:
|
||||
|
||||
- Your role is downgraded to a role with fewer privileges
|
||||
- Your account is (b)locked
|
||||
- You revoke your consent for FreeSewing to process your data
|
||||
|
||||
<Note>
|
||||
This is not an exhaustive list. For example, if we find your use of our API to
|
||||
be excessive, we might also revoke your API keys to shield us from the
|
||||
financial impact of your use of our API.
|
||||
</Note>
|
||||
|
||||
|
104
markdown/dev/reference/backend/api/en.md
Normal file
104
markdown/dev/reference/backend/api/en.md
Normal file
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
title: REST API
|
||||
---
|
||||
|
||||
This is the reference documentation for the FreeSewing backend REST API.
|
||||
|
||||
<Fixme>
|
||||
This documentation is under construction as we are re-working this API for v3.
|
||||
</Fixme>
|
||||
|
||||
## Purpose of this API
|
||||
|
||||
This API is how one can interact with the FreeSewing backend data.
|
||||
That means the data for our users, including all people they have added to
|
||||
their profile, as well as all of the patterns they have saved to our profile.
|
||||
|
||||
This API also manages subscriptions to our newsletter, and a number of other
|
||||
automation tasks such as creating issues on Github.
|
||||
|
||||
## Authentication
|
||||
|
||||
This API is not accessible without authentication.
|
||||
|
||||
The FreeSewing backend API allows two types of authentication:
|
||||
|
||||
- JSON Web Tokens (jwt): This is typically used to authenticate humans in a
|
||||
browser session.
|
||||
- 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.
|
||||
|
||||
Apart from the handlful of API endpoints that are accessible without
|
||||
authentication (typically the ones dealing with the signup flow), this API has
|
||||
a variant for each route depending on what authentication you want to use:
|
||||
|
||||
- `/some/route/jwt` : Authenticate with JWT
|
||||
- `/some/route/key` : Authenticate with an API key and secret
|
||||
|
||||
### 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*.
|
||||
|
||||
To get a token, you must first authenticate at the `/login` endpoint with username and password.
|
||||
You will receive a token in the response.
|
||||
|
||||
In subsequent API calls, you must then include this token in the `Authorization` header prefixed by `Bearer`. Liek his:
|
||||
|
||||
```js
|
||||
const account = await axios.get(
|
||||
`https://backend.freesewing.org/account/jwt`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### API key authentication
|
||||
|
||||
The combination API key & secret serves as a username & password for HTTP basic authentication.
|
||||
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.
|
||||
|
||||
On the plus side, sending a username and password with a request 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:
|
||||
|
||||
```sh
|
||||
curl -u api-key-here:api-secret-here \
|
||||
https://backend.freesewing.org/account/key
|
||||
```
|
||||
|
||||
## Privilege levels for user roles and API keys
|
||||
|
||||
The privilege level is an intiger from `0` to `8`. The higher the number, the higher the priviledge.
|
||||
|
||||
User accounts have a `role` that determines their privilege level.
|
||||
The available roles (with their privilege levels) are:
|
||||
|
||||
- **user**: `4`
|
||||
- **bughunter**: `5`
|
||||
- **support**: `6`
|
||||
- **admin**: `8`
|
||||
|
||||
These roles are used when using JWT authentication, as that's typically used by humans.
|
||||
When using API keys, the privilege level can be set on the API key itself in a more granular way.
|
||||
|
||||
The table below lists the priviledge of all levels as well as their corresponding <small><small><b>`role`</b></small></small>:
|
||||
|
||||
| Level | Abilities | <small><small>`user`</small></small> | <small><small>`bughunter`</small></small> | <small><small>`support`</small></small> | <small><small>`admin`</small></small> |
|
||||
| --: | -- | :--: | :--: | :--: | :--: |
|
||||
| `0` | authenticate | ✅ | ✅ | ✅ | ✅ |
|
||||
| `1` | **read** measurements and patterns | ✅ | ✅ | ✅ | ✅ |
|
||||
| `2` | **read all** account data | ✅ | ✅ | ✅ | ✅ |
|
||||
| `3` | **write** measurements or patterns | ✅ | ✅ | ✅ | ✅ |
|
||||
| `4` | **write all** account data | ✅ | ✅ | ✅ | ✅ |
|
||||
| `5` | **read** measurements or patterns of **other users** | ❌ | ✅ | ✅ | ✅ |
|
||||
| `6` | **read all** account data of **other users** | ❌ | ❌ | ✅ | ✅ |
|
||||
| `7` | **write** access through **specific support methods** | ❌ | ❌ | ✅ | ✅ |
|
||||
| `8` | impersonate other user, full write access | ❌ | ❌ | ❌ | ✅ |
|
||||
|
||||
## API Routes
|
||||
|
||||
<ReadMore />
|
5
markdown/dev/reference/backend/en.md
Normal file
5
markdown/dev/reference/backend/en.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: FreeSewing backend
|
||||
---
|
||||
|
||||
FIXME: Explain what this is about
|
|
@ -10,7 +10,7 @@ export function ApikeyController() {}
|
|||
* Create API key
|
||||
*
|
||||
* This is the endpoint that handles creation of API keys/tokens
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
* See: https://freesewing.dev/reference/backend/api/apikey
|
||||
*/
|
||||
ApikeyController.prototype.create = async (req, res, tools) => {
|
||||
const Apikey = new ApikeyModel(tools)
|
||||
|
@ -22,8 +22,22 @@ ApikeyController.prototype.create = async (req, res, tools) => {
|
|||
/*
|
||||
* Read API key
|
||||
*
|
||||
* This is the endpoint that handles reading of API keys/tokens
|
||||
* See: https://freesewing.dev/reference/backend/api
|
||||
* This is the endpoint that handles creation of API keys/tokens
|
||||
* See: https://freesewing.dev/reference/backend/api/apikey
|
||||
*/
|
||||
ApikeyController.prototype.read = async (req, res, tools) => {
|
||||
const Apikey = new ApikeyModel(tools)
|
||||
await Apikey.readIfAllowed({ id: req.params.id }, req.user)
|
||||
|
||||
return Apikey.sendResponse(res)
|
||||
}
|
||||
|
||||
/*
|
||||
* Read current API key (whoami)
|
||||
*
|
||||
* This is the endpoint that handles reading of the API keys/token used in this
|
||||
* request
|
||||
* See: https://freesewing.dev/reference/backend/api/apikey
|
||||
*/
|
||||
ApikeyController.prototype.whoami = async (req, res, tools) => {
|
||||
const User = new UserModel(tools)
|
||||
|
|
|
@ -46,6 +46,28 @@ ApikeyModel.prototype.verify = async function (key, secret) {
|
|||
return this
|
||||
}
|
||||
|
||||
ApikeyModel.prototype.readIfAllowed = 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, 'permissionLackingToLoadOtherApiKey')
|
||||
}
|
||||
}
|
||||
|
||||
return this.setResponse(200, 'success', {
|
||||
apikey: {
|
||||
key: this.record.id,
|
||||
level: this.record.level,
|
||||
expiresAt: this.record.expiresAt,
|
||||
name: this.record.name,
|
||||
userId: this.record.userId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
ApikeyModel.prototype.read = async function (where) {
|
||||
this.record = await this.prisma.apikey.findUnique({ where })
|
||||
|
||||
|
@ -59,9 +81,9 @@ ApikeyModel.prototype.create = async function ({ body, user }) {
|
|||
if (typeof body.level !== 'number') return this.setResponse(400, 'levelNotNumeric')
|
||||
if (!this.config.apikeys.levels.includes(body.level)) return this.setResponse(400, 'invalidLevel')
|
||||
if (!body.expiresIn) return this.setResponse(400, 'expiresInMissing')
|
||||
if (typeof body.expiresIn !== 'number') return this.setResponse(400, 'expiresInNotNumeric')
|
||||
if (typeof body.expiresIn !== 'number') return this.setResponse(400, 'expiresIsNotNumeric')
|
||||
if (body.expiresIn > this.config.apikeys.maxExpirySeconds)
|
||||
return this.setResponse(400, 'expiresInHigherThanMaximum')
|
||||
return this.setResponse(400, 'expiresIsHigherThanMaximum')
|
||||
|
||||
// Load user making the call
|
||||
await this.User.loadAuthenticatedUser(user)
|
||||
|
|
|
@ -120,7 +120,7 @@ UserModel.prototype.create = async function ({ body }) {
|
|||
await this.mailer.send({
|
||||
template: 'signup',
|
||||
language: this.language,
|
||||
to: 'joost@decock.org', // this.email,
|
||||
to: this.email,
|
||||
replacements: {
|
||||
actionUrl: i18nUrl(this.language, `/confirm/signup/${this.Confirmation.record.id}`),
|
||||
whyUrl: i18nUrl(this.language, `/docs/faq/email/why-signup`),
|
||||
|
|
|
@ -35,12 +35,12 @@ export const closingRow = {
|
|||
</td>
|
||||
</tr>`,
|
||||
text: `
|
||||
{{ closing }}
|
||||
{{{ closing }}}
|
||||
|
||||
{{ greeting }}
|
||||
{{{ greeting }}}
|
||||
joost
|
||||
|
||||
PS: {{ text-ps }} : {{{ text-ps-link }}}`,
|
||||
PS: {{{ text-ps }}} : {{{ supportUrl }}}`,
|
||||
}
|
||||
|
||||
export const headingRow = {
|
||||
|
@ -70,7 +70,7 @@ export const lead1Row = {
|
|||
</p>
|
||||
</td>
|
||||
</tr>`,
|
||||
text: `{{ textLead }}
|
||||
text: `{{{ text-lead }}}
|
||||
{{{ actionUrl }}}
|
||||
`,
|
||||
}
|
||||
|
|
|
@ -21,10 +21,18 @@ export const signup = {
|
|||
${closingRow.html}
|
||||
`),
|
||||
text: wrap.text(`
|
||||
${headingRow.text}
|
||||
${lead1Row.text}
|
||||
${buttonRow.text}
|
||||
${closingRow.text}
|
||||
{{{ heading }}}
|
||||
|
||||
{{{ textLead }}}
|
||||
|
||||
{{{ actionUrl }}}
|
||||
|
||||
{{{ closing }}}
|
||||
|
||||
{{{ greeting }}},
|
||||
joost
|
||||
|
||||
PS: {{{ text-ps }}} : {{{ supportUrl }}}
|
||||
`),
|
||||
}
|
||||
|
||||
|
@ -33,12 +41,14 @@ export const translations = {
|
|||
subject: '[FreeSewing] Confirm your E-mail address to activate your account',
|
||||
heading: 'Welcome to FreeSewing',
|
||||
lead: 'To activate your account, click the big black rectangle below:',
|
||||
textLead: 'To activate your account, click the link below:',
|
||||
button: 'Activate account',
|
||||
closing: "That's all for now.",
|
||||
greeting: 'love',
|
||||
'ps-pre-link': 'FreeSewing is free (duh), but please',
|
||||
'ps-link': 'become a patron',
|
||||
'ps-post-link': 'if you cxan afford it.',
|
||||
'text-ps': 'FreeSewing is free (duh), but please become a patron if you can afford it',
|
||||
...sharedTranslations.en,
|
||||
},
|
||||
// FIXME: Translate German
|
||||
|
@ -46,12 +56,14 @@ export const translations = {
|
|||
subject: '[FreeSewing] Confirm your E-mail address to activate your account',
|
||||
heading: 'Welcome to FreeSewing',
|
||||
lead: 'To activate your account, click the big black rectangle below:',
|
||||
textLead: 'To activate your account, click the link below:',
|
||||
button: 'Activate account',
|
||||
closing: "That's all for now.",
|
||||
greeting: 'love',
|
||||
'ps-pre-link': 'FreeSewing is free (duh), but please',
|
||||
'ps-link': 'become a patron',
|
||||
'ps-post-link': 'if you cxan afford it.',
|
||||
'ps-post-link': 'if you can afford it.',
|
||||
'text-ps': 'FreeSewing is free (duh), but please become a patron if you can afford it',
|
||||
...sharedTranslations.de,
|
||||
},
|
||||
// FIXME: Translate Spanish
|
||||
|
@ -59,12 +71,14 @@ export const translations = {
|
|||
subject: '[FreeSewing] Confirm your E-mail address to activate your account',
|
||||
heading: 'Welcome to FreeSewing',
|
||||
lead: 'To activate your account, click the big black rectangle below:',
|
||||
textLead: 'To activate your account, click the link below:',
|
||||
button: 'Activate account',
|
||||
closing: "That's all for now.",
|
||||
greeting: 'love',
|
||||
'ps-pre-link': 'FreeSewing is free (duh), but please',
|
||||
'ps-link': 'become a patron',
|
||||
'ps-post-link': 'if you cxan afford it.',
|
||||
'ps-post-link': 'if you can afford it.',
|
||||
'text-ps': 'FreeSewing is free (duh), but please become a patron if you can afford it',
|
||||
...sharedTranslations.es,
|
||||
},
|
||||
// FIXME: Translate French
|
||||
|
@ -72,24 +86,29 @@ export const translations = {
|
|||
subject: '[FreeSewing] Confirm your E-mail address to activate your account',
|
||||
heading: 'Welcome to FreeSewing',
|
||||
lead: 'To activate your account, click the big black rectangle below:',
|
||||
textLead: 'To activate your account, click the link below:',
|
||||
button: 'Activate account',
|
||||
closing: "That's all for now.",
|
||||
greeting: 'love',
|
||||
'ps-pre-link': 'FreeSewing is free (duh), but please',
|
||||
'ps-link': 'become a patron',
|
||||
'ps-post-link': 'if you can afford it.',
|
||||
'text-ps': 'FreeSewing is free (duh), but please become a patron if you can afford it',
|
||||
...sharedTranslations.fr,
|
||||
},
|
||||
nl: {
|
||||
subject: '[FreeSewing] Bevestig je E-mail adres om je account te activeren',
|
||||
heading: 'Welkom bij FreeSewing',
|
||||
lead: 'Om je account te activeren moet je op de grote zwarte rechthoek hieronder te klikken:',
|
||||
textLead: 'Om je account te activeren moet je op de link hieronder te klikken:',
|
||||
button: 'Account activeren',
|
||||
closing: 'Daarmee is dat ook weer geregeld.',
|
||||
greeting: 'liefs',
|
||||
'ps-pre-link': 'FreeSewing is gratis (echt), maar gelieve',
|
||||
'ps-link': 'ons werk te ondersteunen',
|
||||
'ps-post-link': 'als het even kan.',
|
||||
'text-ps':
|
||||
'FreeSewing is gratis (echt), maar gelieve ons werk te ondersteunen als het even kan',
|
||||
...sharedTranslations.nl,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -300,7 +300,7 @@ describe(`${user} Signup flow and authentication`, () => {
|
|||
})
|
||||
})
|
||||
|
||||
step(`${user} Read API Key with api key (whoami)`, (done) => {
|
||||
step(`${user} Read API Key with KEY (whoami)`, (done) => {
|
||||
chai
|
||||
.request(config.api)
|
||||
.get(`/whoami/key`)
|
||||
|
@ -315,4 +315,36 @@ describe(`${user} Signup flow and authentication`, () => {
|
|||
done()
|
||||
})
|
||||
})
|
||||
|
||||
step(`${user} Read API Key with KEY`, (done) => {
|
||||
chai
|
||||
.request(config.api)
|
||||
.get(`/apikey/${store.apikey.key}/key`)
|
||||
.auth(store.apikey.key, store.apikey.secret)
|
||||
.end((err, res) => {
|
||||
expect(res.status).to.equal(200)
|
||||
expect(res.type).to.equal('application/json')
|
||||
expect(res.charset).to.equal('utf-8')
|
||||
expect(res.body.result).to.equal(`success`)
|
||||
const checks = ['key', 'level', 'expiresAt', 'name', 'userId']
|
||||
checks.forEach((i) => expect(res.body.apikey[i]).to.equal(store.apikey[i]))
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
step(`${user} Read API Key with JWT`, (done) => {
|
||||
chai
|
||||
.request(config.api)
|
||||
.get(`/apikey/${store.apikey.key}/jwt`)
|
||||
.set('Authorization', 'Bearer ' + store.token)
|
||||
.end((err, res) => {
|
||||
expect(res.status).to.equal(200)
|
||||
expect(res.type).to.equal('application/json')
|
||||
expect(res.charset).to.equal('utf-8')
|
||||
expect(res.body.result).to.equal(`success`)
|
||||
const checks = ['key', 'level', 'expiresAt', 'name', 'userId']
|
||||
checks.forEach((i) => expect(res.body.apikey[i]).to.equal(store.apikey[i]))
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -32,3 +32,5 @@ const mdxCustomComponents = (app = false) => ({
|
|||
})
|
||||
|
||||
export default mdxCustomComponents
|
||||
|
||||
//<span className="bg-secondary px-2 mx-1 rounded text-primary-content bg-opacity-80">{children}</span>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue