mirror of
https://codeberg.org/readeck/readeck.git
synced 2025-12-22 13:17:10 +00:00
Updated the API documentation for the new tooling.
- split the massive bookmarks/routes.yaml into several files - moved big markdown contents to individual files - moved the OAuth documentation to the API description - fixed a few minor bugs with schemas - added a "data" definition to file-based import routes
This commit is contained in:
@@ -25,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
allow-server-selection="false"
|
||||
allow-spec-file-download="true"
|
||||
default-schema-tab="schema"
|
||||
info-description-headings-in-navbar="true"
|
||||
load-fonts="false"
|
||||
nav-bg-color="#ffffff"
|
||||
persist-auth="true"
|
||||
|
||||
@@ -7,66 +7,10 @@ openapi: 3.0.0
|
||||
info:
|
||||
version: 1.0.0
|
||||
title: Readeck API
|
||||
description: |
|
||||
# Introduction
|
||||
|
||||
The Readeck API provides REST endpoints that can be used for any purpose, should it be a
|
||||
mobile application, a script, you name it.
|
||||
|
||||
## API Endpoint
|
||||
|
||||
You can access this API on `__BASE_URI__`.
|
||||
|
||||
Most of your requests and responses are using JSON as the exchange format.
|
||||
|
||||
## Test the API
|
||||
|
||||
On this documentation, you can test every route.
|
||||
|
||||
If you don't provide an API token in [Authentication](#auth), you can still test all the routes
|
||||
but note that the given curl examples only work with an API token.
|
||||
|
||||
## Authentication
|
||||
|
||||
### Create your own token
|
||||
|
||||
If you're writing a script for yourself, the easiest way is to
|
||||
[generate an API token](../profile/tokens) that you can use using
|
||||
the `Bearer` HTTP authorization scheme.
|
||||
|
||||
For example, you first request will look like:
|
||||
|
||||
```sh
|
||||
curl -H "Authorization: Bearer <TOKEN>" __BASE_URI__/bookmarks
|
||||
```
|
||||
|
||||
Or, in NodeJS:
|
||||
|
||||
```js
|
||||
fetch("__BASE_URI__/bookmarks", {
|
||||
headers: {
|
||||
"Authorization": "Bearer <TOKEN>",
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### OAuth
|
||||
|
||||
If you're writing an application, Readeck provides the OAuth 2 authorization code flow.
|
||||
|
||||
#### Client Registration
|
||||
|
||||
Before you can send your app's user to the authorization URL, you first need to register a
|
||||
client on the Readeck instance.
|
||||
|
||||
Please refere to the [Client Creation Route](#post-/oauth/client) for more information.
|
||||
|
||||
|
||||
#### Authorization flow
|
||||
|
||||
Once you received a `client_id`, you can build an authorization URL as explained in the
|
||||
[OAuth Token route](#post-/oauth/token). The rest of the process is following the OAuth 2
|
||||
specifications.
|
||||
description:
|
||||
$include:
|
||||
- base.md
|
||||
- oauth.md
|
||||
|
||||
|
||||
servers:
|
||||
@@ -93,20 +37,28 @@ tags:
|
||||
- name: user profile
|
||||
- name: bookmarks
|
||||
- name: bookmark export
|
||||
- name: bookmark sync
|
||||
- name: bookmark sharing
|
||||
- name: bookmark labels
|
||||
- name: bookmark highlights
|
||||
- name: bookmark collections
|
||||
- name: bookmark sync
|
||||
x-tag-expanded: false
|
||||
- name: bookmark import
|
||||
x-tag-expanded: false
|
||||
- name: dev tools
|
||||
- name: oauth client
|
||||
x-tag-expanded: false
|
||||
- name: oauth authorization
|
||||
x-tag-expanded: false
|
||||
- name: oauth client
|
||||
x-tag-expanded: false
|
||||
|
||||
paths:
|
||||
$merge:
|
||||
- "info/routes.yaml#.routes"
|
||||
- "oauth/routes.yaml#.routes"
|
||||
- "profile/routes.yaml#.routes"
|
||||
- "bookmarks/routes.yaml#.routes"
|
||||
- "bookmarks/routes-bookmarks.yaml#.routes"
|
||||
- "bookmarks/routes-collections.yaml#.routes"
|
||||
- "bookmarks/routes-import.yaml#.routes"
|
||||
- "bookmarks/routes-sync.yaml#.routes"
|
||||
- "cookbook/routes.yaml#.routes"
|
||||
|
||||
40
docs/api/base.md
Normal file
40
docs/api/base.md
Normal file
@@ -0,0 +1,40 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: © 2025 Olivier Meunier <olivier@neokraft.net>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
# Introduction
|
||||
|
||||
The Readeck API provides REST endpoints that can be used for any purpose, should it be a mobile application, a script, you name it.
|
||||
|
||||
## API Endpoint
|
||||
|
||||
You can access this API on `__BASE_URI__`.
|
||||
|
||||
Most of your requests and responses are using JSON as the exchange format.
|
||||
|
||||
## Test the API
|
||||
|
||||
On this documentation, you can test every route.
|
||||
|
||||
If you don't provide an API token in [Authentication](#auth), you can still test all the routes but note that the given curl examples only work with an API token.
|
||||
|
||||
# Token Authentication
|
||||
|
||||
If you're writing a script for yourself, the easiest way is to [generate an API token](../profile/tokens) that you can use using the `Bearer` HTTP authorization scheme.
|
||||
|
||||
For example, you first request will look like:
|
||||
|
||||
```sh
|
||||
curl -H "Authorization: Bearer <TOKEN>" __BASE_URI__/profile
|
||||
```
|
||||
|
||||
Or, in NodeJS:
|
||||
|
||||
```js
|
||||
fetch("__BASE_URI__/profile", {
|
||||
headers: {
|
||||
"Authorization": "Bearer <TOKEN>",
|
||||
},
|
||||
})
|
||||
```
|
||||
84
docs/api/bookmarks/doc-sync.md
Normal file
84
docs/api/bookmarks/doc-sync.md
Normal file
@@ -0,0 +1,84 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: © 2025 Olivier Meunier <olivier@neokraft.net>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
This route returns a `multipart/mixed` response with all the bookmarks passed in `id` (or all of them if unset).
|
||||
|
||||
Here is an example:
|
||||
|
||||
```
|
||||
--910345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b
|
||||
Bookmark-Id: VnopmpKQ3CmQ6apY9mgDws
|
||||
Content-Disposition: attachment; filename="info.json"
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Date: 2025-06-20T10:53:47Z
|
||||
Filename: info.json
|
||||
Last-Modified: 2025-07-03T12:49:30Z
|
||||
Location: http://localhost:8000/api/bookmarks/VnopmpKQ3CmQ6apY9mgDws
|
||||
Type: json
|
||||
|
||||
...content
|
||||
|
||||
--965dd10345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b
|
||||
Bookmark-Id: VnopmpKQ3CmQ6apY9mgDws
|
||||
Content-Disposition: attachment; filename="index.html"
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Date: 2025-06-20T10:53:47Z
|
||||
Filename: index.html
|
||||
Last-Modified: 2025-07-03T12:49:30Z
|
||||
Type: html
|
||||
|
||||
...content
|
||||
|
||||
--965dd10345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b
|
||||
Bookmark-Id: VnopmpKQ3CmQ6apY9mgDws
|
||||
Content-Disposition: attachment; filename="image.jpeg"
|
||||
Content-Length: 86745
|
||||
Content-Type: image/jpeg
|
||||
Filename: image.jpeg
|
||||
Group: image
|
||||
Location: http://localhost:8000/bm/Vn/VnopmpKQ3CmQ6apY9mgDws/img/image.jpeg
|
||||
Path: image.jpeg
|
||||
Type: resource
|
||||
|
||||
...content
|
||||
|
||||
--965dd10345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b
|
||||
Bookmark-Id: VnopmpKQ3CmQ6apY9mgDws
|
||||
Content-Disposition: attachment; filename="Wj66qLatSeikPc31FwvqyS.jpg"
|
||||
Content-Length: 171749
|
||||
Content-Type: image/jpeg
|
||||
Filename: Wj66qLatSeikPc31FwvqyS.jpg
|
||||
Group: embedded
|
||||
Location: http://localhost:8000/bm/Vn/VnopmpKQ3CmQ6apY9mgDws/_resources/Wj66qLatSeikPc31FwvqyS.jpg
|
||||
Path: Wj66qLatSeikPc31FwvqyS.jpg
|
||||
Type: resource
|
||||
|
||||
...content
|
||||
|
||||
--965dd10345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b--
|
||||
```
|
||||
|
||||
|
||||
For each bookmark, there will be entries with a `Type` header:
|
||||
|
||||
- a `Type: json` entry, controlled by `with_json`. It contains the same output as an API bookmark information.
|
||||
- a `Type: html` entry, controlled by `with_html`. It contains the HTML content (article), if any.
|
||||
- a `Type: markdown` entry, controlled by `with_markdown`. It contains the bookmark converted to Markdown.
|
||||
- several `Type: resource` entries, controlled by `with_resources`. Each entry is a resource (icon, images, article images).
|
||||
|
||||
Each entry has a `Bookmark-Id` attribute that indicates the bookmark it belongs to.
|
||||
|
||||
Each `Type: resource` entry contains a `Path` header that's based on the `resource_prefix`
|
||||
parameter.
|
||||
|
||||
Each `Type: resource` entry contains a `Group` header that can take the following values:
|
||||
|
||||
- `icon`: the bookmark's icon,
|
||||
- `image`: the bookmark's image (main picture for photo types, and placeholder for videos),
|
||||
- `thumbnail`: thumbnail of the image,
|
||||
- `embedded`: included in the article itself.
|
||||
|
||||
The response's content is a stream and should be processed while the data is coming, part by
|
||||
part.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
# SPDX-FileCopyrightText: © 2023 Olivier Meunier <olivier@neokraft.net>
|
||||
# SPDX-FileCopyrightText: © 2025 Olivier Meunier <olivier@neokraft.net>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
@@ -32,29 +32,11 @@ withAnnotation:
|
||||
type: string
|
||||
format: short-uid
|
||||
|
||||
withCollection:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: Collection ID
|
||||
schema:
|
||||
type: string
|
||||
format: short-uid
|
||||
|
||||
withMultipartImport:
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: "#components/schemas/baseImport"
|
||||
|
||||
|
||||
routes:
|
||||
/bookmarks:
|
||||
get:
|
||||
tags: [bookmarks]
|
||||
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.paginated"
|
||||
@@ -151,7 +133,7 @@ routes:
|
||||
type: string
|
||||
|
||||
responses:
|
||||
'200':
|
||||
200:
|
||||
description: List of bookmark items
|
||||
content:
|
||||
application/json:
|
||||
@@ -177,7 +159,7 @@ routes:
|
||||
$ref: "#/components/schemas/bookmarkCreate"
|
||||
|
||||
responses:
|
||||
"202":
|
||||
202:
|
||||
headers:
|
||||
Bookmark-Id:
|
||||
schema:
|
||||
@@ -197,7 +179,7 @@ routes:
|
||||
description: Retrieves a saved bookmark
|
||||
|
||||
responses:
|
||||
"200":
|
||||
200:
|
||||
description: Bookmark details
|
||||
content:
|
||||
application/json:
|
||||
@@ -222,7 +204,7 @@ routes:
|
||||
$ref: "#/components/schemas/bookmarkUpdate"
|
||||
|
||||
responses:
|
||||
"200":
|
||||
200:
|
||||
description: Bookmark updated
|
||||
content:
|
||||
application/json:
|
||||
@@ -255,7 +237,7 @@ routes:
|
||||
This route returns the bookmark's article if it exists.
|
||||
|
||||
responses:
|
||||
"200":
|
||||
200:
|
||||
description: |
|
||||
A `text/html` response, containing the article body.
|
||||
Please note that it's only the fragment and not a full HTML document.
|
||||
@@ -287,7 +269,7 @@ routes:
|
||||
enum: [epub, md]
|
||||
|
||||
responses:
|
||||
"200":
|
||||
200:
|
||||
content:
|
||||
application/epub+zip:
|
||||
schema:
|
||||
@@ -310,7 +292,7 @@ routes:
|
||||
description: This route produces a publicly accessible link to share a bookmark.
|
||||
|
||||
responses:
|
||||
"200":
|
||||
200:
|
||||
description: Public link information
|
||||
content:
|
||||
application/json:
|
||||
@@ -336,149 +318,13 @@ routes:
|
||||
$ref: "#/components/schemas/bookmarkShareEmail"
|
||||
|
||||
responses:
|
||||
"200":
|
||||
200:
|
||||
description: Message sent
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/message"
|
||||
|
||||
/bookmarks/sync:
|
||||
get:
|
||||
tags: [bookmark sync]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
|
||||
summary: Bookmark Sync List
|
||||
description: |
|
||||
This route returns a non-paginated list of all bookmarks ordered by last update dates.
|
||||
|
||||
You can retrieve only bookmarks updated and deleted since a given date by using the
|
||||
`since` parameter. Please note that without the parameter, it only returns updated
|
||||
bookmarks.
|
||||
|
||||
parameters:
|
||||
- name: since
|
||||
in: query
|
||||
description: A datetime to retrieve updated and deleted IDs including and after this value.
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
responses:
|
||||
'200':
|
||||
description: Item list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/bookmarkSyncList"
|
||||
|
||||
post:
|
||||
tags: [bookmark sync]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
|
||||
summary: Bookmark Sync
|
||||
description: |
|
||||
This then returns a `multipart/mixed` response with all the bookmarks passed in `id` (or all of them if unset).
|
||||
|
||||
Here is an example:
|
||||
|
||||
```
|
||||
--965dd10345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b
|
||||
Bookmark-Id: VnopmpKQ3CmQ6apY9mgDws
|
||||
Content-Disposition: attachment; filename="info.json"
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Date: 2025-06-20T10:53:47Z
|
||||
Filename: info.json
|
||||
Last-Modified: 2025-07-03T12:49:30Z
|
||||
Location: http://localhost:8000/api/bookmarks/VnopmpKQ3CmQ6apY9mgDws
|
||||
Type: json
|
||||
|
||||
...content
|
||||
|
||||
--965dd10345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b
|
||||
Bookmark-Id: VnopmpKQ3CmQ6apY9mgDws
|
||||
Content-Disposition: attachment; filename="index.html"
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Date: 2025-06-20T10:53:47Z
|
||||
Filename: index.html
|
||||
Last-Modified: 2025-07-03T12:49:30Z
|
||||
Type: html
|
||||
|
||||
...content
|
||||
|
||||
--965dd10345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b
|
||||
Bookmark-Id: VnopmpKQ3CmQ6apY9mgDws
|
||||
Content-Disposition: attachment; filename="image.jpeg"
|
||||
Content-Length: 86745
|
||||
Content-Type: image/jpeg
|
||||
Filename: image.jpeg
|
||||
Group: image
|
||||
Location: http://localhost:8000/bm/Vn/VnopmpKQ3CmQ6apY9mgDws/img/image.jpeg
|
||||
Path: image.jpeg
|
||||
Type: resource
|
||||
|
||||
...content
|
||||
|
||||
--965dd10345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b
|
||||
Bookmark-Id: VnopmpKQ3CmQ6apY9mgDws
|
||||
Content-Disposition: attachment; filename="Wj66qLatSeikPc31FwvqyS.jpg"
|
||||
Content-Length: 171749
|
||||
Content-Type: image/jpeg
|
||||
Filename: Wj66qLatSeikPc31FwvqyS.jpg
|
||||
Group: embedded
|
||||
Location: http://localhost:8000/bm/Vn/VnopmpKQ3CmQ6apY9mgDws/_resources/Wj66qLatSeikPc31FwvqyS.jpg
|
||||
Path: Wj66qLatSeikPc31FwvqyS.jpg
|
||||
Type: resource
|
||||
|
||||
...content
|
||||
|
||||
--965dd10345ce0f660bda92b9e8a1192532f999a51151dccb7227d784049b--
|
||||
```
|
||||
|
||||
|
||||
For each bookmark, there will be entries with a `Type` header:
|
||||
|
||||
- a `Type: json` entry, controlled by `with_json`. It contains the same output as an API bookmark information.
|
||||
- a `Type: html` entry, controlled by `with_html`. It contains the HTML content (article), if any.
|
||||
- a `Type: markdown` entry, controlled by `with_markdown`. It contains the bookmark converted to Markdown.
|
||||
- several `Type: resource` entries, controlled by `with_resources`. Each entry is a resource (icon, images, article images).
|
||||
|
||||
Each entry has a `Bookmark-Id` attribute that indicates the bookmark it belongs to.
|
||||
|
||||
Each `Type: resource` entry contains a `Path` header that's based on the `resource_prefix`
|
||||
parameter.
|
||||
|
||||
Each `Type: resource` entry contains a `Group` header that can take the following values:
|
||||
|
||||
- `icon`: the bookmark's icon,
|
||||
- `image`: the bookmark's image (main picture for photo types, and placeholder for videos),
|
||||
- `thumbnail`: thumbnail of the image,
|
||||
- `embedded`: included in the article itself.
|
||||
|
||||
The response's content is a stream and should be processed while the data is coming, part by
|
||||
part.
|
||||
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/bookmarkSyncParams"
|
||||
|
||||
responses:
|
||||
'200':
|
||||
description: Item list
|
||||
content:
|
||||
multipart/mixed:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
|
||||
/bookmarks/labels:
|
||||
get:
|
||||
tags: [bookmark labels]
|
||||
@@ -490,7 +336,7 @@ routes:
|
||||
This route returns all the labels associated to a bookmark for the current user.
|
||||
|
||||
responses:
|
||||
"200":
|
||||
200:
|
||||
description: Label list
|
||||
content:
|
||||
application/json:
|
||||
@@ -513,7 +359,7 @@ routes:
|
||||
This route returns information about a given bookmark label.
|
||||
|
||||
responses:
|
||||
"200":
|
||||
200:
|
||||
description: Label information
|
||||
content:
|
||||
application/json:
|
||||
@@ -539,7 +385,7 @@ routes:
|
||||
$ref: "#/components/schemas/labelUpdate"
|
||||
|
||||
responses:
|
||||
"200":
|
||||
200:
|
||||
description: Label renamed
|
||||
|
||||
delete:
|
||||
@@ -554,7 +400,7 @@ routes:
|
||||
Please note that it does not remove the bookmarks themselves.
|
||||
|
||||
responses:
|
||||
"204":
|
||||
204:
|
||||
description: Label removed
|
||||
|
||||
/bookmarks/annotations:
|
||||
@@ -626,7 +472,7 @@ routes:
|
||||
$ref: "#/components/schemas/annotationCreate"
|
||||
|
||||
responses:
|
||||
"201":
|
||||
201:
|
||||
description: Highlight created
|
||||
content:
|
||||
application/json:
|
||||
@@ -680,235 +526,4 @@ routes:
|
||||
|
||||
responses:
|
||||
"204":
|
||||
description: Highlight removed
|
||||
|
||||
/bookmarks/collections:
|
||||
get:
|
||||
tags: [bookmark collections]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.paginated"
|
||||
|
||||
summary: Collection List
|
||||
description: |
|
||||
This route returns all the current user's collections.
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Collection list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/collectionInfo"
|
||||
|
||||
post:
|
||||
tags: [bookmark collections]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.created"
|
||||
|
||||
summary: Collection Create
|
||||
description: |
|
||||
This route creates a new collection.
|
||||
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/collectionCreate"
|
||||
|
||||
/bookmarks/collections/{id}:
|
||||
$merge:
|
||||
- "#.withCollection"
|
||||
|
||||
get:
|
||||
tags: [bookmark collections]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
|
||||
summary: Collection Details
|
||||
description: |
|
||||
This route returns a given collection information.
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Collection information
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/collectionInfo"
|
||||
|
||||
patch:
|
||||
tags: [bookmark collections]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
|
||||
summary: Collection Update
|
||||
description: |
|
||||
This route updates a given collection. It returns a mapping of updated fields.
|
||||
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/collectionUpdate"
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Updated fields
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/collectionSummary"
|
||||
|
||||
delete:
|
||||
tags: [bookmark collections]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
|
||||
summary: Collection Delete
|
||||
description: |
|
||||
This route deletes a given collection.
|
||||
|
||||
responses:
|
||||
"204":
|
||||
description: Collection deleted
|
||||
|
||||
/bookmarks/import/browser:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
summary: Import Browser Bookmarks
|
||||
description: |
|
||||
This route creates bookmarks from an HTML file generated by an export of a browser's
|
||||
bookmarks.
|
||||
|
||||
/bookmarks/import/csv:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
summary: Import CSV files
|
||||
description: |
|
||||
This route creates bookmarks from a CSV file. Compatible with Instapaper.
|
||||
|
||||
The uploaded file must contain a first row with the column names. Column names are case insensitive and, except for url, every column is optional.
|
||||
|
||||
Here are the columns you can set:
|
||||
|
||||
| Field | Alias | Description
|
||||
| ------------------------ | :------------- | :----------
|
||||
| `url` (**required**) | | Link address
|
||||
| `title` | | Bookmark title
|
||||
| `state` | `folder` | Bookmark's archived state; only valid value is "archive"
|
||||
| `created` | `timestamp` | Creation date, can be a UNIX timestamp or an RFC-3339 formatted date
|
||||
| `labels` | `tags` | A JSON encoded list of labels
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
url,title,state,created,labels
|
||||
https://www.the-reframe.com/all-in-the-same-boat/,"All In The Same Boat",,2025-01-12T10:45:56,"[""label 1"",""label 2""]"
|
||||
```
|
||||
|
||||
/bookmarks/import/goodlinks:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
summary: Import GoodLinks files.
|
||||
description: |
|
||||
This route creates bookmarks from a GoodLinks export file.
|
||||
|
||||
/bookmarks/import/linkwarden:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
summary: Import Linkwarden files.
|
||||
description: |
|
||||
This route creates bookmarks from a Linkwarden export file.
|
||||
|
||||
/bookmarks/import/pocket-file:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
summary: Import Pocket Saves
|
||||
description: |
|
||||
This route creates bookmarks from an HTML file generated by Pocket export tool.
|
||||
Go to [https://getpocket.com/export](https://getpocket.com/export) to generate
|
||||
such a file.
|
||||
|
||||
/bookmarks/import/readwise:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
summary: Import Readwise files.
|
||||
description: |
|
||||
This route creates bookmarks from a Readwise export file (CSV).
|
||||
|
||||
/bookmarks/import/text:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
summary: Import a Text File
|
||||
description: |
|
||||
This route creates bookmarks from a text file that contains one URL
|
||||
per line.
|
||||
|
||||
/bookmarks/import/wallabag:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
|
||||
summary: Import Wallabag Articles
|
||||
description: |
|
||||
This route imports articles from Wallabag using its API.
|
||||
|
||||
You must create an API client in Wallabag and use its "Client ID" and "Client Secret"
|
||||
in this route's payload.
|
||||
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/wallabagImport"
|
||||
description: Highlight removed
|
||||
111
docs/api/bookmarks/routes-collections.yaml
Normal file
111
docs/api/bookmarks/routes-collections.yaml
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
# SPDX-FileCopyrightText: © 2025 Olivier Meunier <olivier@neokraft.net>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
withCollection:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: Collection ID
|
||||
schema:
|
||||
type: string
|
||||
format: short-uid
|
||||
|
||||
routes:
|
||||
/bookmarks/collections:
|
||||
get:
|
||||
tags: [bookmark collections]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.paginated"
|
||||
|
||||
summary: Collection List
|
||||
description: |
|
||||
This route returns all the current user's collections.
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Collection list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/collectionInfo"
|
||||
|
||||
post:
|
||||
tags: [bookmark collections]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.created"
|
||||
|
||||
summary: Collection Create
|
||||
description: |
|
||||
This route creates a new collection.
|
||||
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/collectionCreate"
|
||||
|
||||
/bookmarks/collections/{id}:
|
||||
$merge:
|
||||
- "#.withCollection"
|
||||
|
||||
get:
|
||||
tags: [bookmark collections]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
|
||||
summary: Collection Details
|
||||
description: |
|
||||
This route returns a given collection information.
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Collection information
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/collectionInfo"
|
||||
|
||||
patch:
|
||||
tags: [bookmark collections]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
|
||||
summary: Collection Update
|
||||
description: |
|
||||
This route updates a given collection. It returns a mapping of updated fields.
|
||||
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/collectionUpdate"
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Updated fields
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/collectionSummary"
|
||||
|
||||
delete:
|
||||
tags: [bookmark collections]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
|
||||
summary: Collection Delete
|
||||
description: |
|
||||
This route deletes a given collection.
|
||||
|
||||
responses:
|
||||
"204":
|
||||
description: Collection deleted
|
||||
154
docs/api/bookmarks/routes-import.yaml
Normal file
154
docs/api/bookmarks/routes-import.yaml
Normal file
@@ -0,0 +1,154 @@
|
||||
---
|
||||
# SPDX-FileCopyrightText: © 2025 Olivier Meunier <olivier@neokraft.net>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
withMultipartImport:
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: "#components/schemas/baseImport"
|
||||
- properties:
|
||||
data:
|
||||
type: string
|
||||
format: binary
|
||||
description: Import file
|
||||
|
||||
routes:
|
||||
/bookmarks/import/browser:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
summary: Import Browser Bookmarks
|
||||
description: |
|
||||
This route creates bookmarks from an HTML file generated by an export of a browser's
|
||||
bookmarks.
|
||||
|
||||
/bookmarks/import/csv:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
|
||||
summary: Import CSV files
|
||||
description: |
|
||||
This route creates bookmarks from a CSV file. Compatible with Instapaper.
|
||||
|
||||
The uploaded file must contain a first row with the column names. Column names are case insensitive and, except for url, every column is optional.
|
||||
|
||||
Here are the columns you can set:
|
||||
|
||||
| Field | Alias | Description
|
||||
| ------------------------ | :------------- | :----------
|
||||
| `url` (**required**) | | Link address
|
||||
| `title` | | Bookmark title
|
||||
| `state` | `folder` | Bookmark's archived state; only valid value is "archive"
|
||||
| `created` | `timestamp` | Creation date, can be a UNIX timestamp or an RFC-3339 formatted date
|
||||
| `labels` | `tags` | A JSON encoded list of labels
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
url,title,state,created,labels
|
||||
https://www.the-reframe.com/all-in-the-same-boat/,"All In The Same Boat",,2025-01-12T10:45:56,"[""label 1"",""label 2""]"
|
||||
```
|
||||
|
||||
/bookmarks/import/goodlinks:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
summary: Import GoodLinks files.
|
||||
description: |
|
||||
This route creates bookmarks from a GoodLinks export file.
|
||||
|
||||
/bookmarks/import/linkwarden:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
summary: Import Linkwarden files.
|
||||
description: |
|
||||
This route creates bookmarks from a Linkwarden export file.
|
||||
|
||||
/bookmarks/import/pocket-file:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
summary: Import Pocket Saves
|
||||
description: |
|
||||
This route creates bookmarks from an HTML file generated by Pocket export tool.
|
||||
Go to [https://getpocket.com/export](https://getpocket.com/export) to generate
|
||||
such a file.
|
||||
|
||||
/bookmarks/import/readwise:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
summary: Import Readwise files.
|
||||
description: |
|
||||
This route creates bookmarks from a Readwise export file (CSV).
|
||||
|
||||
/bookmarks/import/text:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
- "#.withMultipartImport"
|
||||
|
||||
summary: Import a Text File
|
||||
description: |
|
||||
This route creates bookmarks from a text file that contains one URL
|
||||
per line.
|
||||
|
||||
/bookmarks/import/wallabag:
|
||||
post:
|
||||
tags: [bookmark import]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
- "traits.yaml#.deferred"
|
||||
|
||||
summary: Import Wallabag Articles
|
||||
description: |
|
||||
This route imports articles from Wallabag using its API.
|
||||
|
||||
You must create an API client in Wallabag and use its "Client ID" and "Client Secret"
|
||||
in this route's payload.
|
||||
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/wallabagImport"
|
||||
63
docs/api/bookmarks/routes-sync.yaml
Normal file
63
docs/api/bookmarks/routes-sync.yaml
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
# SPDX-FileCopyrightText: © 2025 Olivier Meunier <olivier@neokraft.net>
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
routes:
|
||||
/bookmarks/sync:
|
||||
get:
|
||||
tags: [bookmark sync]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
|
||||
summary: Bookmark Sync List
|
||||
description: |
|
||||
This route returns a non-paginated list of all bookmarks ordered by last update dates.
|
||||
|
||||
You can retrieve only bookmarks updated and deleted since a given date by using the
|
||||
`since` parameter. Please note that without the parameter, it only returns updated
|
||||
bookmarks.
|
||||
|
||||
parameters:
|
||||
- name: since
|
||||
in: query
|
||||
description: A datetime to retrieve updated and deleted IDs including and after this value.
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
responses:
|
||||
'200':
|
||||
description: Item list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/bookmarkSyncList"
|
||||
|
||||
post:
|
||||
tags: [bookmark sync]
|
||||
$merge:
|
||||
- "traits.yaml#.authenticated"
|
||||
- "traits.yaml#.validator"
|
||||
|
||||
summary: Bookmark Sync
|
||||
description:
|
||||
$include: "bookmarks/doc-sync.md"
|
||||
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/bookmarkSyncParams"
|
||||
|
||||
responses:
|
||||
'200':
|
||||
description: Item list
|
||||
content:
|
||||
multipart/mixed:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
@@ -142,12 +142,11 @@ routes:
|
||||
get:
|
||||
tags: [dev tools]
|
||||
$merge:
|
||||
- "traits.yaml#.admin-only"
|
||||
- "traits.yaml#.authenticated"
|
||||
|
||||
summary: Extract Link
|
||||
description: |
|
||||
**NOTE: Only available to users in the admin group.**
|
||||
|
||||
This route extracts a link and returns the extraction result.
|
||||
|
||||
You can pass an `Accept` header to the request, with one of the following values:
|
||||
@@ -163,12 +162,11 @@ routes:
|
||||
post:
|
||||
tags: [dev tools]
|
||||
$merge:
|
||||
- "traits.yaml#.admin-only"
|
||||
- "traits.yaml#.authenticated"
|
||||
|
||||
summary: Extract Link with content
|
||||
description: |
|
||||
**NOTE: Only available to users in the admin group.**
|
||||
|
||||
This route extracts a link and returns the extraction result.
|
||||
|
||||
You can pass an `Accept` header to the request, with one of the following values:
|
||||
@@ -205,12 +203,11 @@ routes:
|
||||
get:
|
||||
tags: [dev tools]
|
||||
$merge:
|
||||
- "traits.yaml#.admin-only"
|
||||
- "traits.yaml#.authenticated"
|
||||
|
||||
summary: List test URLs
|
||||
description: |
|
||||
**NOTE: Only available for users in the admin group.**
|
||||
|
||||
Lists test URLs gathered from `pkg/extract/contentscripts/assets/site-config`
|
||||
|
||||
responses:
|
||||
|
||||
@@ -9,6 +9,9 @@ routes:
|
||||
get:
|
||||
tags: [info]
|
||||
security: []
|
||||
x-badges:
|
||||
- color: green
|
||||
label: public
|
||||
|
||||
summary: Information
|
||||
description: |
|
||||
|
||||
268
docs/api/oauth.md
Normal file
268
docs/api/oauth.md
Normal file
@@ -0,0 +1,268 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: © 2025 Olivier Meunier <olivier@neokraft.net>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
# Authentication with OAuth
|
||||
|
||||
If you're writing an application that requires a user to grant the application permission to access its Readeck instance, you should not ask a user to create an API Token but, instead, implement the necessary OAuth flow so your application can retrieve a token in a user friendly way.
|
||||
|
||||
## Client Registration
|
||||
|
||||
Before you can start the authorization flow, you first need to register a client on the Readeck instance.
|
||||
|
||||
Readeck implement [OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591) and [OAuth 2.0 Dynamic Client Registration Management Protocol](https://datatracker.ietf.org/doc/html/rfc7592).
|
||||
|
||||
You can register a client by querying the [Client Creation Route](#post-/oauth/client).
|
||||
|
||||
Upon registration, you'll receive a `client_id` and a `registration_access_token`. You'll need them if you want to fetch, update or delete the client later. You must store this information as safely as a password.
|
||||
|
||||
Client registration flow:
|
||||
```
|
||||
+---------+ +---------------+
|
||||
| Client | | Registration |
|
||||
+---------+ +---------------+
|
||||
| |
|
||||
| Client Registration Request |
|
||||
| POST /api/oauth/client |
|
||||
|------------------------------------>|
|
||||
| |
|
||||
| Client Information Response |
|
||||
|<------------------------------------|
|
||||
| |
|
||||
```
|
||||
|
||||
Once you have a client, you can retrieve its information, update it or delete it. See:
|
||||
|
||||
- [Client Info](#get-/oauth/client/-id-)
|
||||
- [Client Update](#put-/oauth/client/-id-)
|
||||
- [Client Delete](#delete-/oauth/client/-id-)
|
||||
|
||||
Client management flow:
|
||||
```
|
||||
+---------+ +---------------+
|
||||
| Client | | Registration |
|
||||
+---------+ +---------------+
|
||||
| |
|
||||
| Client Information Request |
|
||||
| GET /api/oauth/client/{id} |
|
||||
|------------------------------------>|
|
||||
| |
|
||||
| Client Information Response |
|
||||
|<------------------------------------|
|
||||
| |
|
||||
| Read or Update Request |
|
||||
| GET or PUT /api/oauth/client/{id} |
|
||||
|------------------------------------>|
|
||||
| |
|
||||
| Client Information Response |
|
||||
|<------------------------------------|
|
||||
| |
|
||||
| Delete Request |
|
||||
| DELETE /api/oauth/client/{id} |
|
||||
|------------------------------------>|
|
||||
| |
|
||||
| Delete Confirmation |
|
||||
|<------------------------------------|
|
||||
| |
|
||||
```
|
||||
|
||||
Here's, in Javascript, an example of a client flow for an app:
|
||||
|
||||
```js
|
||||
async function clientFlow() {
|
||||
// Where you store the client_id and registration_access_token
|
||||
const appVersion = "1.1.0"
|
||||
const store = {}
|
||||
let rsp
|
||||
|
||||
if (!store.clientID) {
|
||||
// New client, create one
|
||||
rsp = await fetch(
|
||||
"__BASE_URI__/oauth/client",
|
||||
{
|
||||
"method": "POST",
|
||||
"body": json.Stringify({
|
||||
"client_name": "My new client",
|
||||
"client_uri": "https://example.org/",
|
||||
"redirect_uris": [
|
||||
"https://example.org/callback"
|
||||
],
|
||||
"software_id": "some-uuid",
|
||||
"software_version": "1.0.0",
|
||||
})
|
||||
},
|
||||
)
|
||||
let data = await rsp.json()
|
||||
store.clientID = data["client_id"]
|
||||
store.registrationAccessToken = data["registration_access_token"]
|
||||
|
||||
// we're done
|
||||
return
|
||||
}
|
||||
|
||||
// We have a client id, check if we need to update it
|
||||
rsp = await fetch(
|
||||
`__BASE_URI__/oauth/client/${store.clientID}`,
|
||||
{
|
||||
"headers": {"Authorization": `Bearer ${store.registrationAccessToken}`}
|
||||
},
|
||||
)
|
||||
let data = await rsp.json()
|
||||
|
||||
if (data["software_version"] != appVersion) {
|
||||
// We need to update the client
|
||||
rsp = await fetch(
|
||||
`__BASE_URI__/oauth/client/${store.clientID}`,
|
||||
{
|
||||
"method": "PUT",
|
||||
"body": json.Stringify({
|
||||
...data,
|
||||
"software_version": appVersion,
|
||||
}),
|
||||
"headers": {"Authorization": `Bearer ${store.registrationAccessToken}`}
|
||||
},
|
||||
)
|
||||
let data = await rsp.json()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Available Scopes
|
||||
|
||||
An OAuth token grants the application some permissions based on the requested scopes. This are the available scopes you can request:
|
||||
|
||||
| Name | Description
|
||||
| :---------------- | ------------
|
||||
| `bookmarks:read` | Read only access to bookmarks
|
||||
| `bookmarks:write` | Write only access to bookmarks
|
||||
| `profile:read` | Extended profile information
|
||||
|
||||
|
||||
## OAuth Authorization Code Flow
|
||||
|
||||
With the `client_id`, you can use the authorization code flow. You first need to build an authorization URL.
|
||||
|
||||
### Authorization
|
||||
|
||||
The authorization route is: `__ROOT_URI__/authorize` and it receives the following query parameters:
|
||||
|
||||
| Name | Description
|
||||
| :---------------------- | :-----------------------------
|
||||
| `client_id` | OAuth Client ID
|
||||
| `redirect_uri` | Redirection URI (must match exactly one given during client registration)
|
||||
| `scope` | Space separated list of scopes
|
||||
| `code_challenge` | PKCE Challenge (mandatory)
|
||||
| `code_challenge_method` | Only `S256` is allowed
|
||||
| `state` | Optional client state
|
||||
|
||||
Sending a state is not mandatory but strongly advised to ensure that your side of the authorization flow has not been tampered.
|
||||
|
||||
### Authorization result
|
||||
|
||||
Once a user grants or denies an authorization request, it will be redirected to the `redirect_uri` with the following query parameters:
|
||||
|
||||
| Name | Description
|
||||
| :------ | :--------------------------------------------------------------------
|
||||
| `code` | The authorization code that the client must pass to the token request
|
||||
| `state` | The state as initially set by the client
|
||||
|
||||
In case of error (request denied by the user or something else), the redirection contains
|
||||
the following query parameters:
|
||||
|
||||
| Name | Description
|
||||
| :------------------ | :--------------------------------------------------------------------
|
||||
| `error` | Error code (can be `invalid_request` or `access_denied`)
|
||||
| `error_description` | Error description
|
||||
|
||||
Once you receive a code, you can proceed to the [Token Request](#post-/oauth/token) to eventually receive an access token that will let you use the API.
|
||||
|
||||
### PKCE
|
||||
|
||||
The authorization code flow requires that you use [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) with an S256 method only (the "plain" method is not allowed).
|
||||
|
||||
**Important**: The challenge must be base64 encoded, **with URL encoding** and **without padding**.
|
||||
|
||||
Here's a Javascript example of a verifier and challenge generation:
|
||||
|
||||
```js
|
||||
function generateRandomString() {
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
let res = ""
|
||||
const buf = new Uint8Array(64)
|
||||
crypto.getRandomValues(buf)
|
||||
for (let i in buf) {
|
||||
res += alphabet[buf[i] % alphabet.length]
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async function pkceChallengeFromVerifier(v) {
|
||||
const b = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(v))
|
||||
return btoa(String.fromCharCode(...new Uint8Array(b)))
|
||||
.replaceAll("+", "-")
|
||||
.replaceAll("/", "_")
|
||||
.replaceAll("=", "")
|
||||
}
|
||||
|
||||
const verifier = generateRandomString()
|
||||
pkceChallengeFromVerifier(verifier).then(challenge => {
|
||||
console.log(verifier)
|
||||
console.log(challenge)
|
||||
})
|
||||
```
|
||||
|
||||
### Workflow
|
||||
|
||||
Authorization flow:
|
||||
```
|
||||
+-------+ +---------+ +---------------+ +-----+
|
||||
| User | | Client | | Authorization | | API |
|
||||
+-------+ +---------+ +---------------+ +-----+
|
||||
| | | |
|
||||
| Enter instance URL | | |
|
||||
|------------------------>| | |
|
||||
| | | |
|
||||
| | Generate PKCE verifier and challenge | |
|
||||
| |------------------------------------- | |
|
||||
| | | | |
|
||||
| |<------------------------------------ | |
|
||||
| | | |
|
||||
| | Open authorization URL | |
|
||||
| | GET /authorize?... | |
|
||||
| |--------------------------------------------->| |
|
||||
| | | |
|
||||
| Redirect to login/authorization prompt | |
|
||||
|<-----------------------------------------------------------------------| |
|
||||
| | | |
|
||||
| Authorize Client | | |
|
||||
| POST /authorize?... | |
|
||||
|----------------------------------------------------------------------->| |
|
||||
| | | |
|
||||
| | Authorization code | |
|
||||
| |<---------------------------------------------| |
|
||||
| | | |
|
||||
| | Check state | |
|
||||
| |------------ | |
|
||||
| | | | |
|
||||
| |<----------- | |
|
||||
| | | |
|
||||
| | Request Token (with code and verifier) | |
|
||||
| | POST /api/oauth/token | |
|
||||
| |--------------------------------------------->| |
|
||||
| | | |
|
||||
| | | Check PKCE |
|
||||
| | |----------- |
|
||||
| | | | |
|
||||
| | |<---------- |
|
||||
| | | |
|
||||
| | Access Token | |
|
||||
| |<---------------------------------------------| |
|
||||
| | | |
|
||||
| | Request data with Access Token | |
|
||||
| |------------------------------------------------------------->|
|
||||
| | | |
|
||||
| | | Response |
|
||||
| |<-------------------------------------------------------------|
|
||||
| | | |
|
||||
```
|
||||
@@ -34,125 +34,10 @@ routes:
|
||||
|
||||
summary: Client Create
|
||||
description: |
|
||||
This route creates a new OAuth client. You must create a new client before you can request permissions and a token.
|
||||
|
||||
Upon registration, you'll receive a `client_id` and an `registration_access_token`.
|
||||
You'll need them if you want to fetch, update or delete the client later.
|
||||
You must keep this information and store them as safely as a password.
|
||||
This route creates a new OAuth client. You must create a new client before you can request permissions and receive a token.
|
||||
|
||||
This route implements [RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/html/rfc7591).
|
||||
|
||||
Client registration flow:
|
||||
```
|
||||
+---------+ +---------------+
|
||||
| Client | | Registration |
|
||||
+---------+ +---------------+
|
||||
| |
|
||||
| Client Registration Request |
|
||||
| POST /api/oauth/client |
|
||||
|------------------------------------>|
|
||||
| |
|
||||
| Client Information Response |
|
||||
|<------------------------------------|
|
||||
| |
|
||||
```
|
||||
|
||||
Client management flow:
|
||||
```
|
||||
+---------+ +---------------+
|
||||
| Client | | Registration |
|
||||
+---------+ +---------------+
|
||||
| |
|
||||
| Client Information Request |
|
||||
| GET /api/oauth/client/{id} |
|
||||
|------------------------------------>|
|
||||
| |
|
||||
| Client Information Response |
|
||||
|<------------------------------------|
|
||||
| |
|
||||
| Read or Update Request |
|
||||
| GET or PUT /api/oauth/client/{id} |
|
||||
|------------------------------------>|
|
||||
| |
|
||||
| Client Information Response |
|
||||
|<------------------------------------|
|
||||
| |
|
||||
| Delete Request |
|
||||
| DELETE /api/oauth/client/{id} |
|
||||
|------------------------------------>|
|
||||
| |
|
||||
| Delete Confirmation |
|
||||
|<------------------------------------|
|
||||
| |
|
||||
```
|
||||
|
||||
See also:
|
||||
|
||||
- [Client Info](#get-/oauth/client/-id-)
|
||||
- [Client Update](#put-/oauth/client/-id-)
|
||||
- [Client Delete](#delete-/oauth/client/-id-)
|
||||
|
||||
Here's, in Javascript, an example of a client flow for an app:
|
||||
|
||||
```js
|
||||
async function clientFlow() {
|
||||
// Where you store the client_id and registration_access_token
|
||||
const appVersion = "1.1.0"
|
||||
const store = {}
|
||||
let rsp
|
||||
|
||||
if (!store.clientID) {
|
||||
// New client, create one
|
||||
rsp = await fetch(
|
||||
"__BASE_URI__/oauth/client",
|
||||
{
|
||||
"method": "POST",
|
||||
"body": json.Stringify({
|
||||
"client_name": "My new client",
|
||||
"client_uri": "https://example.org/",
|
||||
"redirect_uris": [
|
||||
"https://example.org/callback"
|
||||
],
|
||||
"software_id": "some-uuid",
|
||||
"software_version": "1.0.0",
|
||||
})
|
||||
},
|
||||
)
|
||||
let data = await rsp.json()
|
||||
store.clientID = data["client_id"]
|
||||
store.registrationAccessToken = data["registration_access_token"]
|
||||
|
||||
// we're done
|
||||
return
|
||||
}
|
||||
|
||||
// We have a client id, check if we need to update it
|
||||
rsp = await fetch(
|
||||
`__BASE_URI__/oauth/client/${store.clientID}`,
|
||||
{
|
||||
"headers": {"Authorization": `Bearer ${store.registrationAccessToken}`}
|
||||
},
|
||||
)
|
||||
let data = await rsp.json()
|
||||
|
||||
if (data["software_version"] != appVersion) {
|
||||
// We need to update the client
|
||||
rsp = await fetch(
|
||||
`__BASE_URI__/oauth/client/${store.clientID}`,
|
||||
{
|
||||
"method": "PUT",
|
||||
"body": json.Stringify({
|
||||
...data,
|
||||
"software_version": appVersion,
|
||||
}),
|
||||
"headers": {"Authorization": `Bearer ${store.registrationAccessToken}`}
|
||||
},
|
||||
)
|
||||
let data = await rsp.json()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
@@ -182,7 +67,7 @@ routes:
|
||||
summary: Client Info
|
||||
description: |
|
||||
This route returns a registered client information. You must pass the received `registration_access_token`
|
||||
after creating the client in a `Authorization: Beare registration_access_token` header.
|
||||
after creating the client in a `Authorization: Bearer registration_access_token` header.
|
||||
|
||||
This route implements [RFC 7592: OAuth 2.0 Dynamic Client Registration Management Protocol](https://datatracker.ietf.org/doc/html/rfc7592).
|
||||
|
||||
@@ -250,134 +135,6 @@ routes:
|
||||
A client must call this route once it received the necessary information from the
|
||||
redirect URI generated by the authorization route.
|
||||
|
||||
### Authorization
|
||||
|
||||
The authorization route is: `__ROOT_URI__/authorize` and it receives the following
|
||||
query parameters:
|
||||
|
||||
| Name | Description
|
||||
| :---------------------- | :-----------------------------
|
||||
| `client_id` | OAuth Client ID
|
||||
| `redirect_uri` | Redirection URI
|
||||
| `scope` | Space separated list of scopes
|
||||
| `code_challenge` | PKCE Challenge
|
||||
| `code_challenge_method` | Only `S256` is allowed
|
||||
| `state` | Optional client state
|
||||
|
||||
Sending a state is not mandatory but strongly advised to ensure that your side of the
|
||||
authorization flow has not been tampered.
|
||||
|
||||
### Authorization result
|
||||
|
||||
Once a user grants or denies an authorization request, it will be redirected to the
|
||||
`redirect_uri` with the following query parameters:
|
||||
|
||||
| Name | Description
|
||||
| :------ | :--------------------------------------------------------------------
|
||||
| `code` | The authorization code that the client must pass to the token request
|
||||
| `state` | The state as initially set by the client
|
||||
|
||||
In case of error (request denied by the user or something else), the redirection contains
|
||||
the following query parameters:
|
||||
|
||||
| Name | Description
|
||||
| :------------------ | :--------------------------------------------------------------------
|
||||
| `error` | Error code (can be `invalid_request` or `access_denied`)
|
||||
| `error_description` | Error description
|
||||
|
||||
### PKCE
|
||||
|
||||
The authorization code flow requires that you use [PKCE](https://datatracker.ietf.org/doc/html/rfc7636)
|
||||
with an S256 method only (the "plain" method is not allowed).
|
||||
|
||||
**Important**: The challenge must be base64 encoded, **with URL encoding** and **without padding**.
|
||||
|
||||
Here's a Javascript example of a verifier and challenge generation:
|
||||
|
||||
```js
|
||||
function generateRandomString() {
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
let res = ""
|
||||
const buf = new Uint8Array(64)
|
||||
crypto.getRandomValues(buf)
|
||||
for (let i in buf) {
|
||||
res += alphabet[buf[i] % alphabet.length]
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async function pkceChallengeFromVerifier(v) {
|
||||
const b = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(v))
|
||||
return btoa(String.fromCharCode(...new Uint8Array(b)))
|
||||
.replaceAll("+", "-")
|
||||
.replaceAll("/", "_")
|
||||
.replaceAll("=", "")
|
||||
}
|
||||
|
||||
const verifier = generateRandomString()
|
||||
pkceChallengeFromVerifier(verifier).then(challenge => {
|
||||
console.log(verifier)
|
||||
console.log(challenge)
|
||||
})
|
||||
```
|
||||
|
||||
### Workflow
|
||||
|
||||
Authorization flow:
|
||||
```
|
||||
+-------+ +---------+ +---------------+ +-----+
|
||||
| User | | Client | | Authorization | | API |
|
||||
+-------+ +---------+ +---------------+ +-----+
|
||||
| | | |
|
||||
| Enter instance URL | | |
|
||||
|------------------------>| | |
|
||||
| | | |
|
||||
| | Generate PKCE verifier and challenge | |
|
||||
| |------------------------------------- | |
|
||||
| | | | |
|
||||
| |<------------------------------------ | |
|
||||
| | | |
|
||||
| | Open authorization URL | |
|
||||
| | GET /authorize?... | |
|
||||
| |--------------------------------------------->| |
|
||||
| | | |
|
||||
| Redirect to login/authorization prompt | |
|
||||
|<-----------------------------------------------------------------------| |
|
||||
| | | |
|
||||
| Authorize Client | | |
|
||||
| POST /authorize?... | |
|
||||
|----------------------------------------------------------------------->| |
|
||||
| | | |
|
||||
| | Authorization code | |
|
||||
| |<---------------------------------------------| |
|
||||
| | | |
|
||||
| | Check state | |
|
||||
| |------------ | |
|
||||
| | | | |
|
||||
| |<----------- | |
|
||||
| | | |
|
||||
| | Request Token (with code and verifier) | |
|
||||
| | POST /api/oauth/token | |
|
||||
| |--------------------------------------------->| |
|
||||
| | | |
|
||||
| | | Check PKCE |
|
||||
| | |----------- |
|
||||
| | | | |
|
||||
| | |<---------- |
|
||||
| | | |
|
||||
| | Access Token | |
|
||||
| |<---------------------------------------------| |
|
||||
| | | |
|
||||
| | Request data with Access Token | |
|
||||
| |------------------------------------------------------------->|
|
||||
| | | |
|
||||
| | | Response |
|
||||
| |<-------------------------------------------------------------|
|
||||
| | | |
|
||||
```
|
||||
|
||||
|
||||
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
|
||||
@@ -76,13 +76,13 @@ schemas:
|
||||
description: Client supported response type
|
||||
|
||||
oauthClientUpdate:
|
||||
$merge:
|
||||
- "#.schemas.oauthClientCreate"
|
||||
required: [client_id, client_name, client_uri, software_id, software_version, redirect_uris]
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
description: Client's ID
|
||||
$merge:
|
||||
- "#.schemas.oauthClientCreate.properties"
|
||||
|
||||
oauthClientResponse:
|
||||
properties:
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
admin-only:
|
||||
x-badges:
|
||||
- color: red
|
||||
label: admin only
|
||||
|
||||
authenticated:
|
||||
responses:
|
||||
"401":
|
||||
@@ -21,6 +26,10 @@ authenticated:
|
||||
$ref: "#/components/schemas/message"
|
||||
|
||||
paginated:
|
||||
x-badges:
|
||||
- color: purple
|
||||
label: paginated
|
||||
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
|
||||
Reference in New Issue
Block a user