diff --git a/markdown/dev/reference/backend/api/users/confirm/en.md b/markdown/dev/reference/backend/api/account/confirm/en.md
similarity index 95%
rename from markdown/dev/reference/backend/api/users/confirm/en.md
rename to markdown/dev/reference/backend/api/account/confirm/en.md
index 067995f5f34..cd29e0fbdc1 100644
--- a/markdown/dev/reference/backend/api/users/confirm/en.md
+++ b/markdown/dev/reference/backend/api/account/confirm/en.md
@@ -1,13 +1,13 @@
---
-title: Confirm a User
+title: Confirm an account
---
-Confirms a newly created User.
+Confirms a newly created User account.
If confirmation is successful this will also result in a (passwordless) login.
## Endpoints
-Confirming a new User is possible via this endpoint:
+Confirming a new User account is possible via this endpoint:
| Method | Path | Authentication |
| --------: | :--- | :------------- |
@@ -18,7 +18,7 @@ Confirming a new User is possible via this endpoint:
## Request url
The url should contain the confirmation ID that was E-mailed to the E-mail
-address used for the signup. It replaces the `:id` placeholder in the
+address used for the signup. It replaces the `:id` placeholder in the
[endpoint listed above](#endpoints).
## Request body
diff --git a/markdown/dev/reference/backend/api/users/create/en.md b/markdown/dev/reference/backend/api/account/create/en.md
similarity index 87%
rename from markdown/dev/reference/backend/api/users/create/en.md
rename to markdown/dev/reference/backend/api/account/create/en.md
index 3ef6291a6f6..6016aeaeb2f 100644
--- a/markdown/dev/reference/backend/api/users/create/en.md
+++ b/markdown/dev/reference/backend/api/account/create/en.md
@@ -1,13 +1,13 @@
---
-title: Create a User
+title: Create an account
---
-Creates a new User. The User account will remain inactive
-until [it is confirmed](/reference/backend/api/users/confirm).
+Creates a new User account. The User account will remain inactive
+until [it is confirmed](/reference/backend/api/account/confirm).
## Endpoints
-Creating a new User is possible via this endpoint:
+Creating a new User account is possible via this endpoint:
| Method | Path | Authentication |
| --------: | :--- | :------------- |
diff --git a/markdown/dev/reference/backend/api/users/en.md b/markdown/dev/reference/backend/api/account/en.md
similarity index 92%
rename from markdown/dev/reference/backend/api/users/en.md
rename to markdown/dev/reference/backend/api/account/en.md
index 2c1d523d394..40ff4728e60 100644
--- a/markdown/dev/reference/backend/api/users/en.md
+++ b/markdown/dev/reference/backend/api/account/en.md
@@ -1,8 +1,12 @@
---
-title: Users
+title: Account
---
-A User holds the account data for a FreeSewing user.
+From an end-user's point of view, their account holds all of their data. From
+an API point of view, these endpoints deal with data in the User table.
+
+As the endpoints typically use `/account` we tend to use _account_ more often
+than _user_.
## Endpoints
diff --git a/markdown/dev/reference/backend/api/account/login/en.md b/markdown/dev/reference/backend/api/account/login/en.md
new file mode 100644
index 00000000000..a5324fb61c9
--- /dev/null
+++ b/markdown/dev/reference/backend/api/account/login/en.md
@@ -0,0 +1,113 @@
+---
+title: Login
+---
+
+Login as a User with username and password, and optional MFA token.
+
+## Endpoints
+
+Password-based login is possible via this endpoint:
+
+| Method | Path | Authentication |
+| --------: | :--- | :------------- |
+| | `/login` | None |
+
+This endpoint requires no authentication
+
+## Request body
+
+| Property | Type | Description |
+| ----------: | :------- | :---------- |
+| `username` | `string` | The E-mail address of the User |
+| `password` | `boolean`| The language code for the User |
+| `token` | `boolean`| The MFA token |
+
+An MFA token is required (only) when the User enabled MFA
+
+## Response status codes
+
+Possible status codes for these endpoints are:
+
+| Status code | Description |
+| ----------: | :---------- |
+| | success |
+| | the request was malformed |
+| | authentication failed |
+| | MFA token missing |
+| | server error |
+
+
+If the status code is not the `error` property
+in the response body should indicate the nature of the problem.
+
+
+## Response body
+
+| Value | Type | Description |
+| ------------------- | -------- | ----------- |
+| `result` | String | Either `success` or `error` |
+| `error` | String | Will give info on the nature of the error. Only set if an error occured. |
+| `token` | String | A JSON web token (JWT) token to authenticate with |
+| `account.id` | Number | The ID of the User |
+| `account.bio` | String | The bio of the User |
+| `account.consent` | Number | The consent given by the User |
+| `account.control` | Number | The control desired by the User |
+| `account.createdAt` | String | Date string indicating the moment the User was created |
+| `account.email` | String | The E-mail address currently tied to the User |
+| `account.github` | String | The Github username of the User |
+| `account.img` | String | The URL to the image stored with this User |
+| `account.imperial` | Boolean| Whether or not the User prefers imperial units |
+| `account.initial` | String | The E-mail address that the User was created with |
+| `account.language` | String | The language preferred by the user |
+| `account.lastLogin` | String | Date string indicating them moment the User last logged in |
+| `account.mfaEnabled`| Boolean| Whether or not the User has MFA enabled |
+| `account.newsletter`| Boolean| Whether or not the User is subscribed to the FreeSewing newsletter |
+| `account.patron` | Number | The level of patronage the user provides to FreeSewing |
+| `account.role` | String | The role of the User |
+| `account.status` | Number | The status of the user |
+| `account.updatedAt` | String | Date string indicating the last time the User was updated |
+| `account.username` | String | The username of the User |
+| `account.lusername` | String | A lowercased version of the username of the User |
+
+## Example request
+
+```js
+const signup = await axios.post(
+ 'https://backend.freesewing.org/signup',
+ {
+ username: "jimmy",
+ language: "I like big bewbs and I just can't lie",
+ token: 231586
+ }
+)
+```
+
+## Example response
+```200.json
+{
+ "result": "success",
+ "token": "eyJhbGciOiJIUzI1NiIsInR5c...truncated",
+ "account": {
+ "id": 14,
+ "bio": "",
+ "consent": 1,
+ "control": 1,
+ "createdAt": "2022-11-19T18:15:22.642Z",
+ "email": "test_54c6856275aaa8a1@freesewing.dev",
+ "github": "",
+ "img": "https://freesewing.org/avatar.svg",
+ "imperial": false,
+ "initial": "test_54c6856275aaa8a1@freesewing.dev",
+ "language": "en",
+ "lastLogin": "2022-11-19T18:15:22.668Z",
+ "mfaEnabled": false,
+ "newsletter": false,
+ "patron": 0,
+ "role": "user",
+ "status": 1,
+ "updatedAt": "2022-11-19T18:15:22.668Z",
+ "username": "jimmy",
+ "lusername": "jimmy"
+ }
+}
+```
diff --git a/markdown/dev/reference/backend/api/account/mfa/en.md b/markdown/dev/reference/backend/api/account/mfa/en.md
new file mode 100644
index 00000000000..a1245131625
--- /dev/null
+++ b/markdown/dev/reference/backend/api/account/mfa/en.md
@@ -0,0 +1,214 @@
+---
+title: MFA
+---
+
+Enable of disable Multi-Factor Authentication (MFA) on the User account.
+
+- [Setup MFA](#setup-mfa)
+- [Confirm MFA](#confirm-mfa)
+- [Disable MFA](#disable-mfa)
+
+## Endpoints
+
+Enabling, confirming, and disabling MFA is all possible via this endpoint:
+
+| Method | Path | Authentication |
+| --------: | :--- | :------------- |
+| | `/account/mfa/jwt` | [JSON Web Token](/reference/backend/api/authentication#jwt-authentication) |
+| | `/account/mfa/key` | [API Key & Secret](/reference/backend/api/authentication#key-authentication) |
+
+## Setup MFA
+
+### Request body
+
+| Property | Type | Description |
+| ----------: | :------- | :---------- |
+| `mfa` | `boolean`| Set to `true` to enable MFA |
+
+### Response status codes
+
+Possible status codes for this endpoints are:
+
+| Status code | Description |
+| ----------: | :---------- |
+| | success |
+| | the request was malformed |
+| | authentication failed |
+| | access denied |
+| | server error |
+
+
+If the status code is not the `error` property
+in the response body should indicate the nature of the problem.
+
+
+### Response body
+
+| Value | Type | Description |
+| -------------- | -------- | ----------- |
+| `result` | String | Either `success` or `error` |
+| `error` | String | Will give info on the nature of the error. Only set if an error occured. |
+| `mfa.secret` | String | The shared secret for generating one-time password (OTP) tokens |
+| `mfa.otpauth` | String | The OTP Auth uri that is encoded in the QR code |
+| `mfa.qrcode` | String | SVG to display a QR code with the otpauth uri encoded |
+
+
+##### Styling the SVG
+The SVG returned by the backend uses `currentColor` for the QR code, so you can
+style it with CSS if you embed it in the page.
+
+
+### Example request
+
+```js
+const mfa = await axios.post(
+ 'https://backend.freesewing.org/account/mfa/jwt',
+ { mfa: true },
+ {
+ headers: {
+ Authorization: `Bearer ${token}`
+ }
+ }
+)
+```
+
+### Example response
+```200.json
+{
+ "result": "success",
+ "mfa": {
+ "secret": "KBTSKUKRDJPEGCZK",
+ "otpauth": "otpauth://totp/FreeSewing:user-294?secret=KBTSKUKRDJPEGCZK&period=30&digits=6&algorithm=SHA1&issuer=FreeSewing",
+ "qrcode": "\n"
+ }
+}
+```
+
+## Confirm MFA
+
+To confirm the MFA, we need to provide an MFA token to ensure the user can
+generate them.
+
+### Request body
+
+| Property | Type | Description |
+| ----------: | :------- | :---------- |
+| `mfa` | `boolean`| Must be set to `true` to confirm MFA |
+| `secret` | `boolean`| The secret returned when setting up MFA |
+| `token` | `boolean`| Must be set to `true` to confirm MFA |
+
+### Response status codes
+
+Possible status codes for this endpoints are:
+
+| Status code | Description |
+| ----------: | :---------- |
+| | success |
+| | the request was malformed |
+| | authentication failed |
+| | access denied |
+| | server error |
+
+
+If the status code is not the `error` property
+in the response body should indicate the nature of the problem.
+
+
+### Response body
+
+| Value | Type | Description |
+| -------------- | -------- | ----------- |
+| `result` | String | Either `success` or `error` |
+| `error` | String | Will give info on the nature of the error. Only set if an error occured. |
+
+### Example request
+
+```js
+import { authenticator } from '@otplib/preset-default'
+
+const confirm = await axios.post(
+ 'https://backend.freesewing.org/account/mfa/jwt',
+ {
+ mfa: true,
+ secret: mfa.secret,
+ token: authenticator.generate(mfa.secret)
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${token}`
+ }
+ }
+)
+```
+
+### Example response
+
+```200.json
+{
+ "result": "success",
+}
+```
+## Disable MFA
+
+To disable MFA, you need to provide both the account password and a valid token.
+
+### Request body
+
+| Property | Type | Description |
+| ----------: | :------- | :---------- |
+| `mfa` | `boolean`| Must be set to `false` to disable MFA |
+| `password` | `boolean`| The User's password |
+| `token` | `boolean`| Must be set to `true` to confirm MFA |
+
+### Response status codes
+
+Possible status codes for this endpoints are:
+
+| Status code | Description |
+| ----------: | :---------- |
+| | success |
+| | the request was malformed |
+| | authentication failed |
+| | access denied |
+| | server error |
+
+
+If the status code is not the `error` property
+in the response body should indicate the nature of the problem.
+
+
+### Response body
+
+| Value | Type | Description |
+| -------------- | -------- | ----------- |
+| `result` | String | Either `success` or `error` |
+| `error` | String | Will give info on the nature of the error. Only set if an error occured. |
+
+### Example request
+
+```js
+import { authenticator } from '@otplib/preset-default'
+
+const confirm = await axios.post(
+ 'https://backend.freesewing.org/account/mfa/jwt',
+ {
+ mfa: false,
+ password: "I like big bewbs and I just can't lie",
+ token: authenticator.generate(mfa.secret)
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${token}`
+ }
+ }
+)
+```
+
+### Example response
+
+```200.json
+{
+ "result": "success",
+}
+```
+
diff --git a/markdown/dev/reference/backend/api/account/update/en.md b/markdown/dev/reference/backend/api/account/update/en.md
new file mode 100644
index 00000000000..de5afd79151
--- /dev/null
+++ b/markdown/dev/reference/backend/api/account/update/en.md
@@ -0,0 +1,126 @@
+---
+title: Update account
+---
+
+Updates an existing User account.
+
+## Access control
+
+- [Permission level](/reference/backend/api/rbac) `4` or higher is required to update your own User account
+- [Permission level](/reference/backend/api/rbac) `8` is required to update **another user's** account
+
+## Endpoints
+
+Updating an existing User account is possible via these endpoints:
+
+| Method | Path | Authentication |
+| --------: | :--- | :------------- |
+| | `/account/jwt` | [JSON Web Token](/reference/backend/api/authentication#jwt-authentication) |
+| | `/account/key` | [API Key & Secret](/reference/backend/api/authentication#key-authentication) |
+
+## Request body
+
+| Property | Type | Description |
+| ----------: | :------- | :---------- |
+| `bio` | `string` | The User's bio |
+| `consent` | `string` | A number that indicates [the consent given by the user](/reference/backend/api/account#the-consent-field-is-about-data-protection) |
+| `control` | `string` | A number that indicates [the level of control the user prefers](/reference/backend/api/account#the-control-field-is-about-keeping-it-simple) |
+| `github` | `string` | The User's username on Github |
+| `imperial` | `boolean`| Whether or not the User prefers imperial units |
+| `newsletter`| `boolean`| Whether this Person prefers imperial measurements (`true`) or not (`false`) |
+| `img` | `string` | An image [data-uri][duri] to store with this Person |
+| `password` | `string` | The (new) password for the User |
+| `username` | `string` | The (new) username for the User |
+
+## Response status codes
+
+Possible status codes for these endpoints are:
+
+| Status code | Description |
+| ----------: | :---------- |
+| | success |
+| | the request was malformed |
+| | the request lacks authentication |
+| | authentication failed |
+| | server error |
+
+
+If the status code is not the `error` property
+in the response body should indicate the nature of the problem.
+
+
+## Response body
+
+| Value | Type | Description |
+| ------------------- | -------- | ----------- |
+| `result` | String | Either `success` or `error` |
+| `error` | String | Will give info on the nature of the error. Only set if an error occured. |
+| `account.id` | Number | The ID of the User |
+| `account.bio` | String | The bio of the User |
+| `account.consent` | Number | The consent given by the User |
+| `account.control` | Number | The control desired by the User |
+| `account.createdAt` | String | Date string indicating the moment the User was created |
+| `account.email` | String | The E-mail address currently tied to the User |
+| `account.github` | String | The Github username of the User |
+| `account.img` | String | The URL to the image stored with this User |
+| `account.imperial` | Boolean| Whether or not the User prefers imperial units |
+| `account.initial` | String | The E-mail address that the User was created with |
+| `account.language` | String | The language preferred by the user |
+| `account.lastLogin` | String | Date string indicating them moment the User last logged in |
+| `account.mfaEnabled`| Boolean| Whether or not the User has MFA enabled |
+| `account.newsletter`| Boolean| Whether or not the User is subscribed to the FreeSewing newsletter |
+| `account.patron` | Number | The level of patronage the user provides to FreeSewing |
+| `account.role` | String | The role of the User |
+| `account.status` | Number | The status of the user |
+| `account.updatedAt` | String | Date string indicating the last time the User was updated |
+| `account.username` | String | The username of the User |
+| `account.lusername` | String | A lowercased version of the username of the User |
+
+## Example request
+
+```js
+const udpate = await axios.put(
+ 'https://backend.freesewing.org/account/jwt',
+ {
+ bio: "I like imperial now",
+ imperial: true,
+ username: "ImperialLover"
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${token}`
+ }
+ }
+)
+```
+
+## Example response
+```200.json
+{
+ "result": "success",
+ "account": {
+ "id": 14,
+ "bio": "I like imperial now",
+ "consent": 1,
+ "control": 1,
+ "createdAt": "2022-11-19T18:15:22.642Z",
+ "email": "test_54c6856275aaa8a1@freesewing.dev",
+ "github": "",
+ "img": "https://freesewing.org/avatar.svg",
+ "imperial": true,
+ "initial": "test_54c6856275aaa8a1@freesewing.dev",
+ "language": "en",
+ "lastLogin": "2022-11-19T18:15:22.668Z",
+ "mfaEnabled": false,
+ "newsletter": false,
+ "patron": 0,
+ "role": "user",
+ "status": 1,
+ "updatedAt": "2022-11-19T18:15:22.668Z",
+ "username": "ImperialLover",
+ "lusername": "imperiallover"
+ }
+}
+```
+
+[duri]: https://en.wikipedia.org/wiki/Data_URI_scheme
diff --git a/markdown/dev/reference/backend/api/users/login/en.md b/markdown/dev/reference/backend/api/users/login/en.md
deleted file mode 100644
index c3db833cd46..00000000000
--- a/markdown/dev/reference/backend/api/users/login/en.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: User login
----
-
-Create docs
diff --git a/markdown/dev/reference/backend/api/users/mfa/en.md b/markdown/dev/reference/backend/api/users/mfa/en.md
deleted file mode 100644
index 97f32ce9c4b..00000000000
--- a/markdown/dev/reference/backend/api/users/mfa/en.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: MFA
----
-
-Create docs
diff --git a/markdown/dev/reference/backend/api/users/update/en.md b/markdown/dev/reference/backend/api/users/update/en.md
deleted file mode 100644
index d19feb64745..00000000000
--- a/markdown/dev/reference/backend/api/users/update/en.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update a User
----
-
-Create docs