Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/metastable-void/alarkhabil-server
Backend API server for Al Arkhabil, the independent thought publication platform.
https://github.com/metastable-void/alarkhabil-server
api backend backend-api cms cms-backend rust
Last synced: 16 days ago
JSON representation
Backend API server for Al Arkhabil, the independent thought publication platform.
- Host: GitHub
- URL: https://github.com/metastable-void/alarkhabil-server
- Owner: metastable-void
- License: apache-2.0
- Created: 2023-10-20T15:52:06.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2023-11-30T11:10:03.000Z (about 1 year ago)
- Last Synced: 2024-05-01T23:27:44.657Z (8 months ago)
- Topics: api, backend, backend-api, cms, cms-backend, rust
- Language: Rust
- Homepage:
- Size: 123 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Al Arkhabil API server
Backend API server for Al Arkhabil, the independent thought publication platform.
* [Frontend code Github](https://github.com/metastable-void/alarkhabil-frontend)
## About this API
* Strings are always in UTF-8.
* Maximum post size in Markdown is 100kB.
* Maximum bio/channel description size in Markdown is 4kB.
* File uploads/user icons are not supported in the first release, but will be supported in future releases.
* API pagination is not supported first. Dangerous queries are limited to 1000 entries.
* Body text (including channel/author descriptions) strings are assumed to be in Markdown. Titles and names are not. Currently this server does not parse Markdown.### Tokens
* __Invite making token__: hex-encoded string of random data. Can be used to create a new invite.
* __Admin token__: hex-encoded string of random data. Can be used for administrative actions (e.g. deletion of users, etc.).
* __Invite token__: base64-encoded string containing message signed by the server. Can be used for requesting a new account. Can be parsed freely to get the invited user's UUID.## List of v1 endpoints
All endpoints are below `/api/v1/`.
Regex for ValidDnsToken(): `^[a-z0-9]+(-[a-z0-9]+)*$`
* __Account__ means the authenticating user's account.
* __self__ means the author object of the authenticating user.
* __Author__ is an author object.Method | URL | Auth | Invariant | Input
-------|-----|------|-----------|------
GET | invite/new | `{invite making token}` | - | Query: `token`
POST | account/new | Self-signed by new public key | Public key does not exist yet on DB | Signed JSON data (POST)
POST | account/check_credentials | **Pubkey account auth** | NotDeleted(Account) | Signed JSON data (POST)
POST | account/change_credentials | **Pubkey account auth** (Signed by old public key) | NotDeleted(Account) && Valid signature by new public key included | Signed JSON data (POST)
POST | account/delete | **Pubkey account auth** | NotDeleted(Account) | Signed JSON data (POST)
POST | admin/meta/update | `{admin token}` | ValidDnsToken(`page_name`) | Query: `token`; Plain JSON data (POST)
POST | admin/meta/delete | `{admin token}` | MetaPageExists(`page_name`) | Query: `token`, `page_name`; Empty POST data
POST | admin/author/delete | `{admin token}` | AuthorExists(`uuid`) | Query: `token`, `uuid`; Empty POST data
POST | admin/channel/delete | `{admin token}` | ChannelExists(`uuid`) | Query: `token`, `uuid`; Empty POST data
POST | admin/post/delete | `{admin token}` | PostExists(`uuid`) | Query: `token`, `uuid`; Empty POST data
POST | self/update | **Pubkey account auth** | NotDeleted(Account) | Signed JSON data (POST)
POST | channel/new | **Pubkey account auth** | NotDeleted(Account) && !ChannelExists(`handle`) && ValidDnsToken(`handle`) | Signed JSON data (POST)
POST | channel/update | **Pubkey account auth** | NotDeleted(Account) && NotDeleted(Channel) && Owns(Channel) && NoConflict(`handle`) && ValidDnsToken(`handle`) | Signed JSON data (POST)
POST | channel/delete | **Pubkey account auth** | NotDeleted(Account) && NotDeleted(Channel) && Owns(Channel) | Signed JSON data (POST)
POST | channel/add_author | **Pubkey account auth** | NotDeleted(Account) && NotDelete(Channel) && Owns (Channel) && NotDeleted(Author) && Account != Author | Signed JSON data (POST)
POST | channel/remove_author | **Pubkey account auth** | NotDeleted(Account) && NotDelete(Channel) && Owns (Channel) && NotDeleted(Author) && Account != Author | Signed JSON data (POST)
POST | post/new | **Pubkey account auth** | NotDeleted(Account) && NotDeleted(Channel) && Owns(Channel) | Signed JSON data (POST)
POST | post/update | **Pubkey account auth** | NotDeleted(Account) && NotDeleted(Channel) && NotDeleted(Post) && Owns(Channel) | Signed JSON data (POST)
POST | post/delete | **Pubkey account auth** | NotDeleted(Account) && NotDeleted(Channel) && NotDeleted(Post) && Owns(Channel) | Signed JSON data (POST)
GET | meta/info | - | MetaPageExists(`page_name`) | Query: `page_name`
GET | meta/list | - | - | -
GET | author/info | - | NotDeleted(Author) | Query: `uuid`
GET | author/list | - | NotDeleted(Author) | -
GET | author/channels | - | NotDeleted(Author) && NotDeleted(Channel) | Query: `uuid`
GET | author/posts | - | NotDeleted(Author) && NotDeleted(Channel) && NotDeleted(Post) && NotDeleted(Revision) | Query: `uuid`
GET | channel/info | - | NotDeleted(Channel) | Query: `uuid` or `handle`
GET | channel/list | - | NotDeleted(Channel) | -
GET | channel/authors | - | NotDeleted(Channel) && NotDeleted(Author) | Query: `uuid`
GET | channel/posts | - | NotDeleted(Channel) && NotDeleted(Post) | Query: `uuid`
GET | post/info | - | NotDeleted(Post) && NotDeleted(Channel) [ && HasUndeleted(Revision) ] | Query: `uuid`
GET | post/list | - | NotDeleted(Post) && NotDeleted(Channel) [ && HasUndeleted(Revision) ] | -
GET | tag/list | - | NotDeleted(Post) && NotDeleted(Channel) && HasUndeleted(Revision) | -
GET | tag/posts | - | NotDeleted(Post) && NotDeleted(Channel) && HasUndeleted(Revision) | -## Invites v1
### GET /api/v1/invite/new
The administrator would have access to their *invite making token*.
The administrator uses the *invite making token* to make a request of this type, and they will get an invite token in base64, which they can tell someone.
**Note:** This endpoint uses GET method because it does not change the state on the server (in the first design).
**Query format:** `?token={invite making token}`
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Response:
```
HTTP/1.1 200
{
"status": "ok",
"invite": ""
}
```## Accounts v1
### POST /api/v1/account/new
The user who wants to create an account, creates an ed25519 key pair and signs the folowing payload with the private key.
The payload contains the new account's name and an invite token from `GET /api/v1/invite/new`.
The signed message contains the user's ed25519 public key.The response will contain the new account's UUID.
**Post data:** Alarkhabil-ed25519-signed JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Payload:
```
{
"command": "account_new",
"name": "",
"invite": ""
}
```Response example:
```
HTTP/1.1 200
{
"status": "ok",
"uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```### POST /api/v1/account/check_credentials
Basically a no-op authenticated request. Returns an error if authentication fails.
**Post data:** Alarkhabil-ed25519-signed JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Payload:
```
{
"command": "account_check_credentials"
}
```Response example:
```
HTTP/1.1 200
{
"status": "ok"
}
```### POST /api/v1/account/change_credentials
**Post data:** Alarkhabil-ed25519-signed JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Payload:
```
{
"command": "account_change_credentials",
"new_algo": "",
"new_public_key": "",
"signature": ""
}
```Response example:
```
HTTP/1.1 200
{
"status": "ok"
}
```### POST /api/v1/account/delete
**TODO: Is this really needed?**
**Post data:** Alarkhabil-ed25519-signed JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Payload:
```
{
"command": "account_delete"
}
```Response example:
```
HTTP/1.1 200
{
"status": "ok"
}
```## Admin v1
### POST /api/v1/admin/meta/update
Creates or updates a meta page.
**Query format:** `?token={admin token}`
**Post data:** JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Request:
```
{
"page_name": "",
"title": "",
"text": ""
}
```Response:
```
HTTP/1.1 200
{
"status": "ok"
}
```### POST /api/v1/admin/meta/delete
Deletes a meta page. This is irreversible.
**Query format:** `?token={admin token}&page_name={page name}`
**Post data:** none
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Response:
```
HTTP/1.1 200
{
"status": "ok"
}
```### POST /api/v1/admin/author/delete
**Query format:** `?token={admin token}&uuid={author's uuid}`
**Post data:** none
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Response:
```
HTTP/1.1 200
{
"status": "ok"
}
```### POST /api/v1/admin/channel/delete
**Query format:** `?token={admin token}&uuid={channel's uuid}`
**Post data:** none
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Response:
```
HTTP/1.1 200
{
"status": "ok"
}
```### POST /api/v1/admin/post/delete
**Query format:** `?token={admin token}&uuid={post's uuid}`
**Post data:** none
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Response:
```
HTTP/1.1 200
{
"status": "ok"
}
```## Authors' endpoints v1
### POST /api/v1/self/update
**Post data:** Alarkhabil-ed25519-signed JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Payload:
```
{
"command": "self_update",
"name": "",
"description_text": ""
}
```Response example (same as `/api/v1/author/info`):
```
HTTP/1.1 200
{
"uuid": "",
"name": "",
"created_date":
"description_text": ""
}
```### POST /api/v1/channel/new
**Post data:** Alarkhabil-ed25519-signed JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Payload:
```
{
"command": "channel_new",
"handle": "",
"name": "",
"lang": ""
}
```Response example (same as `/api/v1/channel/info`):
```
HTTP/1.1 200
{
"uuid": "",
"handle": "",
"name": "",
"created_date":
"lang": "",
"description_text": ""
}
```### POST /api/v1/channel/update
**Post data:** Alarkhabil-ed25519-signed JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Payload:
```
{
"command": "channel_update",
"uuid": "",
"handle": "",
"name": "",
"lang": "",
"description_text": ""
}
```Response example (same as `/api/v1/channel/info`):
```
HTTP/1.1 200
{
"uuid": "",
"handle": "",
"name": "",
"created_date":
"lang": "",
"description_text": ""
}
```### POST /api/v1/channel/delete
**Post data:** Alarkhabil-ed25519-signed JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Payload:
```
{
"command": "channel_delete",
"uuid": ""
}
```Response example:
```
HTTP/1.1 200
{
"status": "ok"
}
```### POST /api/v1/channel/add_author
You cannot add yourself as an author.
**Post data:** Alarkhabil-ed25519-signed JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Payload:
```
{
"command": "channel_add_author",
"uuid": "",
"author_uuid": ""
}
```Response example:
```
HTTP/1.1 200
{
"status": "ok"
}
```### POST /api/v1/channel/remove_author
You cannot remove yourself from a channel.
**Post data:** Alarkhabil-ed25519-signed JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Payload:
```
{
"command": "channel_remove_author",
"uuid": "",
"author_uuid": ""
}
```Response example:
```
HTTP/1.1 200
{
"status": "ok"
}
```### POST /api/v1/post/new
**Post data:** Alarkhabil-ed25519-signed JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Payload:
```
{
"command": "post_new",
"channel_uuid": "",
"title": "",
"text": "",
"tags": [
"",
...
]
}
```Response example (same as `/api/v1/post/info`):
```
HTTP/1.1 200
{
"post_uuid": "",
"channel": {
"uuid": "",
"handle": "",
"name": "",
"lang": ""
},
"revision_uuid": "",
"revision_date": ,
"title": "",
"author": {
"uuid": "",
"name": ""
},
"revision_text": "",
"tags": [
"",
...
]
}
```### POST /api/v1/post/update
**Post data:** Alarkhabil-ed25519-signed JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Payload:
```
{
"command": "post_update",
"uuid": "",
"title": "",
"text": "",
"tags": [
"",
...
]
}
```Response example (same as `/api/v1/post/info`):
```
HTTP/1.1 200
{
"post_uuid": "",
"channel": {
"uuid": "",
"handle": "",
"name": "",
"lang": ""
},
"revision_uuid": "",
"revision_date": "",
"title": "",
"author": {
"uuid": "",
"name": ""
},
"revision_text": "",
"tags": [
"",
...
]
}
```### POST /api/v1/post/delete
**Post data:** Alarkhabil-ed25519-signed JSON
**Response type:** JSON
Will return **400 Bad Request** for invalid requests.
Payload:
```
{
"command": "post_delete",
"uuid": ""
}
```Response example:
```
HTTP/1.1 200
{
"status": "ok"
}
```## Public endpoints v1
### GET /api/v1/meta/info
**Query format:** `?page_name={meta page name}`
**Response type:** JSON
Response (post found):
```
HTTP/1.1 200
{
"page_name": "",
"updated_date": ,
"title": "",
"text": ""
}
```Response (post not found):
```
HTTP/1.1 404
{
"status": "not found"
}
```### GET /api/v1/meta/list
**Query format:** (none) - TODO: allow paging
**Response type:** JSON
Response:
```
HTTP/1.1 200
[
{
"page_name": "",
"updated_date": ,
"title": ""
},
...
]
```### GET /api/v1/author/info
**Query format:** `?uuid={author uuid}`
**Response type:** JSON
Response (author found):
```
HTTP/1.1 200
{
"uuid": "",
"name": "",
"created_date":
"description_text": ""
}
```Response (author not found or deleted):
```
HTTP/1.1 404
{
"status": "not found"
}
```### GET /api/v1/author/list
The results are ordered with the newest registration first.
**Query format:** (none) - TODO: allow paging
**Response type:** JSON
Response:
```
HTTP/1.1 200
[
{
"uuid": "",
"name": ""
},
...
]
```### GET /api/v1/author/channels
**Query format:** `?uuid={author uuid}`
**Response type:** JSON
Empty array will be returned if no channels are found.
Response (author found):
```
HTTP/1.1 200
[
{
"uuid": "",
"handle": "",
"name": "",
"lang": ""
},
...
]
```Response (author not found or deleted):
```
HTTP/1.1 404
{
"status": "not found"
}
```### GET /api/v1/author/posts
**Query format:** `?uuid={author uuid}`
**Response type:** JSON
Empty array will be returned if no posts are found.
Response (author found):
```
HTTP/1.1 200
[
{
"post_uuid": "",
"revision_uuid": "",
"revision_date": "",
"title": "",
"channel": {
"uuid": "",
"handle": "",
"name": "",
"lang": ""
}
},
...
]
```Response (author not found or deleted):
```
HTTP/1.1 404
{
"status": "not found"
}
```### GET /api/v1/channel/info
**Query format:** `?uuid={channel uuid}`
**Query format:** `?handle={channel handle}`
**Response type:** JSON
Response (channel found):
```
HTTP/1.1 200
{
"uuid": "",
"handle": "",
"name": "",
"created_date":
"lang": "",
"description_text": ""
}
```Response (channel not found or deleted):
```
HTTP/1.1 404
{
"status": "not found"
}
```### GET /api/v1/channel/list
The results are ordered with the newest channel first.
**Query format:** (none) - TODO: allow paging
**Response type:** JSON
Response:
```
HTTP/1.1 200
[
{
"uuid": "",
"handle": "",
"name": "",
"lang": ""
},
...
]
```### GET /api/v1/channel/authors
**Query format:** `?uuid={channel uuid}`
**Response type:** JSON
Response (channel found):
```
HTTP/1.1 200
[
{
"uuid": "",
"name": ""
},
...
]
```Response (channel not found or deleted):
```
HTTP/1.1 404
{
"status": "not found"
}
```### GET /api/v1/channel/posts
**Query format:** `?uuid={channel uuid}`
**Response type:** JSON
Response (channel found):
Empty array will be returned if no posts are found.
```
HTTP/1.1 200
[
{
"post_uuid": "",
"revision_uuid": "",
"revision_date": "",
"title": "",
"author": {
"uuid": "",
"name": ""
}
},
...
]
```Response (channel not found or deleted):
```
HTTP/1.1 404
{
"status": "not found"
}
```### GET /api/v1/post/info
**Query format:** `?uuid={post uuid}`
**Response type:** JSON
Response (post found):
```
HTTP/1.1 200
{
"post_uuid": "",
"channel": {
"uuid": "",
"handle": "",
"name": "",
"lang": ""
},
"revision_uuid": "",
"revision_date": "",
"title": "",
"author": {
"uuid": "",
"name": ""
},
"revision_text": "",
"tags": [
"",
...
]
}
```Response (post not found or deleted):
```
HTTP/1.1 404
{
"status": "not found"
}
```### GET /api/v1/post/list
The results are ordered with the newest post first.
**Query format:** (none) - TODO: allow paging
**Response type:** JSON
Response:
```
HTTP/1.1 200
[
{
"post_uuid": "",
"revision_uuid": "",
"revision_date": "",
"title": "",
"author": {
"uuid": "",
"name": ""
},
"channel": {
"uuid": "",
"handle": "",
"name": "",
"lang": ""
}
},
...
]
```### GET /api/v1/tag/list
**Query format:** (none) - TODO: allow paging
**Response type:** JSON
Response:
```
HTTP/1.1 200
[
{
"tag_name": "",
"page_count":
},
...
]
```### GET /api/v1/tag/posts
The results are ordered with the newest post first.
**Query format:** `?tag_name={tag name}` - TODO: allow paging
**Response type:** JSON
Response:
```
HTTP/1.1 200
[
{
"post_uuid": "",
"revision_uuid": "",
"revision_date": "",
"title": "",
"author": {
"uuid": "",
"name": ""
},
"channel": {
"uuid": "",
"handle": "",
"name": "",
"lang": ""
}
},
...
]
```## Build
```
cargo build
```## Configuration
```
cp ./example.env ./.env
# edit ./.env
```## License
Licensed under the Apache 2.0 license.
### Authors
- [@metastable-void](https://github.com/metastable-void)