# Console routes

Sign-in, account, invitations and sessions of the console and the back office (ADR 0015)

Sign-in, account, invitations and sessions of the console and the back office (ADR 0015). Generated from `openapi.yaml` of games-api 0.7.0: 19 routes.

- [`POST /v1/console/sign-in`](#post-v1-console-sign-in): Sign in with email and password
- [`POST /v1/console/sign-in/totp`](#post-v1-console-sign-in-totp): Finish a sign-in with the 6-digit code of the authenticator
- [`POST /v1/console/sign-out`](#post-v1-console-sign-out): Sign out
- [`GET /v1/console/me`](#get-v1-console-me): The signed-in account
- [`PATCH /v1/console/me`](#patch-v1-console-me): Change the display name
- [`GET /v1/console/invitations/{token}`](#get-v1-console-invitations-token): What an invitation grants, for the accept page
- [`POST /v1/console/invitations/{token}/accept`](#post-v1-console-invitations-token-accept): Accept an invitation; this creates the account and signs it in
- [`POST /v1/console/password/forgot`](#post-v1-console-password-forgot): Email a password reset link
- [`POST /v1/console/password/reset`](#post-v1-console-password-reset): Set a new password from a reset link
- [`PUT /v1/console/password`](#put-v1-console-password): Change the password
- [`POST /v1/console/email`](#post-v1-console-email): Ask to change the email address
- [`POST /v1/console/email/confirm`](#post-v1-console-email-confirm): Confirm a new email address from its link
- [`POST /v1/console/totp`](#post-v1-console-totp): Start a TOTP enrolment
- [`DELETE /v1/console/totp`](#delete-v1-console-totp): Remove TOTP, with a current code
- [`POST /v1/console/totp/confirm`](#post-v1-console-totp-confirm): Confirm the enrolment with a first code; TOTP is on from here
- [`GET /v1/console/sessions`](#get-v1-console-sessions): The account's sessions, newest first
- [`DELETE /v1/console/sessions/{sessionId}`](#delete-v1-console-sessions-sessionid): Revoke one session; revoking the current one signs this browser out
- [`POST /v1/console/impersonation/end`](#post-v1-console-impersonation-end): End an impersonation
- [`POST /v1/console/invitation-requests`](#post-v1-console-invitation-requests): Ask for an invitation, from the form on developers.filudi.com

<RouteHeading id="post-v1-console-sign-in" method="POST" path="/v1/console/sign-in" />

<CodePanel title="POST /v1/console/sign-in">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/console/sign-in' \
      -H 'content-type: application/json' \
      -d '{
      "email": "you@example.com",
      "password": "string"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    const response = await fetch('https://api.filudi.com/v1/console/sign-in', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        email: 'you@example.com',
        password: 'string',
      }),
    });
    const data = await response.json();
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    response = requests.post(
        'https://api.filudi.com/v1/console/sign-in',
        json={
            'email': 'you@example.com',
            'password': 'string',
        },
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Sign in with email and password**

Sets the `filudi_console` cookie (ADR 0015). When the account has TOTP, `totpRequired` is true, `me` is
absent and the cookie holds a pending sign-in for 5 minutes: finish with `POST /v1/console/sign-in/totp`.
An unknown email and a wrong password get the same 401 in the same time. Attempts are limited per email
and per IP in Redis, as `POST /v1/account/sign-in` does.

Credential: none. The route is public.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string (email) | yes |  |
| `password` | string | yes | 1 to 1024 characters; write only |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [SignInResult](/docs/api/reference/schemas/#schema-signinresult) | Signed in, or a TOTP code is due. Headers: `Set-Cookie` (`filudi_console`). |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | "Email or password is wrong.". |
| 403 | none | The account is disabled. |
| 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. |
| 503 | none | Redis is down; the rate limit cannot be checked. |

<RouteHeading id="post-v1-console-sign-in-totp" method="POST" path="/v1/console/sign-in/totp" />

<CodePanel title="POST /v1/console/sign-in/totp">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/console/sign-in/totp' \
      -b "filudi_console=$FILUDI_SESSION" \
      -H 'origin: https://developers.filudi.com' \
      -H 'content-type: application/json' \
      -d '{
      "code": "string"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // The browser sends the filudi_console cookie and the Origin header itself.
    const response = await fetch('https://api.filudi.com/v1/console/sign-in/totp', {
      method: 'POST',
      credentials: 'include',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        code: 'string',
      }),
    });
    const data = await response.json();
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    # session: the value of the filudi_console cookie set by POST /v1/console/sign-in
    response = requests.post(
        'https://api.filudi.com/v1/console/sign-in/totp',
        cookies={'filudi_console': session},
        headers={'origin': 'https://developers.filudi.com'},
        json={
            'code': 'string',
        },
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Finish a sign-in with the 6-digit code of the authenticator**

Needs the pending cookie of `POST /v1/console/sign-in`. The code is RFC 6238, 30-second step, one step
of drift either way; a code is accepted once. The fifth wrong code ends the pending sign-in.

Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | [TotpCode](/docs/api/reference/schemas/#schema-totpcode) | yes | Six digits, "123456" or "123 456"; pattern `^[0-9 ]{6,7}$` |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [ConsoleMe](/docs/api/reference/schemas/#schema-consoleme) | Signed in; the cookie now holds a 14-day session. |
| 400 | [Error](/docs/api/reference/schemas/#schema-error) | Wrong code. On the fifth wrong code the pending sign-in is gone, the cookie is cleared and `details.signInEnded` is true. |
| 401 | none | No pending sign-in. |

<RouteHeading id="post-v1-console-sign-out" method="POST" path="/v1/console/sign-out" />

<CodePanel title="POST /v1/console/sign-out">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/console/sign-out' \
      -b "filudi_console=$FILUDI_SESSION" \
      -H 'origin: https://developers.filudi.com'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // The browser sends the filudi_console cookie and the Origin header itself.
    const response = await fetch('https://api.filudi.com/v1/console/sign-out', {
      method: 'POST',
      credentials: 'include',
    });
    // No body: check response.ok.
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    # session: the value of the filudi_console cookie set by POST /v1/console/sign-in
    response = requests.post(
        'https://api.filudi.com/v1/console/sign-out',
        cookies={'filudi_console': session},
        headers={'origin': 'https://developers.filudi.com'},
    )
    response.raise_for_status()
    # No body: the status says it all.
    ```

  </Fragment>
</CodePanel>

**Sign out**

Deletes the session row and clears the cookie. Answers 204 without a session too.

Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead.

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 204 | none | Signed out. |

<RouteHeading id="get-v1-console-me" method="GET" path="/v1/console/me" />

<CodePanel title="GET /v1/console/me">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/console/me' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // The browser sends the filudi_console cookie and the Origin header itself.
    const response = await fetch('https://api.filudi.com/v1/console/me', {
      credentials: 'include',
    });
    const data = await response.json();
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    # session: the value of the filudi_console cookie set by POST /v1/console/sign-in
    response = requests.get(
        'https://api.filudi.com/v1/console/me',
        cookies={'filudi_console': session},
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**The signed-in account**

Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead.

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [ConsoleMe](/docs/api/reference/schemas/#schema-consoleme) | Account. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. |

<RouteHeading id="patch-v1-console-me" method="PATCH" path="/v1/console/me" />

<CodePanel title="PATCH /v1/console/me">
  <Fragment slot="curl">

    ```sh
    curl -X PATCH 'https://api.filudi.com/v1/console/me' \
      -b "filudi_console=$FILUDI_SESSION" \
      -H 'origin: https://developers.filudi.com' \
      -H 'content-type: application/json' \
      -d '{
      "name": "string"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // The browser sends the filudi_console cookie and the Origin header itself.
    const response = await fetch('https://api.filudi.com/v1/console/me', {
      method: 'PATCH',
      credentials: 'include',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        name: 'string',
      }),
    });
    const data = await response.json();
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    # session: the value of the filudi_console cookie set by POST /v1/console/sign-in
    response = requests.patch(
        'https://api.filudi.com/v1/console/me',
        cookies={'filudi_console': session},
        headers={'origin': 'https://developers.filudi.com'},
        json={
            'name': 'string',
        },
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Change the display name**

Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | [AccountName](/docs/api/reference/schemas/#schema-accountname) | yes | Display name, shown to other members and in the audit log; 1 to 80 characters |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [ConsoleMe](/docs/api/reference/schemas/#schema-consoleme) | Account. |
| 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. |

<RouteHeading id="get-v1-console-invitations-token" method="GET" path="/v1/console/invitations/{token}" />

<CodePanel title="GET /v1/console/invitations/{token}">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/console/invitations/string'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    const response = await fetch('https://api.filudi.com/v1/console/invitations/string');
    const data = await response.json();
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    response = requests.get('https://api.filudi.com/v1/console/invitations/string')
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**What an invitation grants, for the accept page**

Credential: none. The route is public.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `token` | path | string | yes | The token of the invitation link |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [InvitationPreview](/docs/api/reference/schemas/#schema-invitationpreview) | Invitation. |
| 404 | none | Unknown, expired, accepted or revoked: one answer for the four. |

<RouteHeading id="post-v1-console-invitations-token-accept" method="POST" path="/v1/console/invitations/{token}/accept" />

<CodePanel title="POST /v1/console/invitations/{token}/accept">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/console/invitations/string/accept' \
      -H 'content-type: application/json' \
      -d '{
      "name": "string",
      "password": "string"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    const response = await fetch('https://api.filudi.com/v1/console/invitations/string/accept', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        name: 'string',
        password: 'string',
      }),
    });
    const data = await response.json();
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    response = requests.post(
        'https://api.filudi.com/v1/console/invitations/string/accept',
        json={
            'name': 'string',
            'password': 'string',
        },
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Accept an invitation; this creates the account and signs it in**

The password follows the policy of `PUT /v1/account/password`: 8 to 128 characters after NFC
normalization, not equal to the email. The token is single use. Sets the `filudi_console` cookie.

Credential: none. The route is public.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `token` | path | string | yes | The token of the invitation link |

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | [AccountName](/docs/api/reference/schemas/#schema-accountname) | yes | Display name, shown to other members and in the audit log; 1 to 80 characters |
| `password` | [NewPassword](/docs/api/reference/schemas/#schema-newpassword) | yes | The policy of `PUT /v1/account/password`: 8 to 128 characters after NFC normalization, not equal to the email; 8 to 128 characters; write only |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [ConsoleMe](/docs/api/reference/schemas/#schema-consoleme) | Account created and signed in. Headers: `Set-Cookie` (`filudi_console`). |
| 400 | none | The password breaks the policy. |
| 404 | none | Unknown, expired, accepted or revoked. |

<RouteHeading id="post-v1-console-password-forgot" method="POST" path="/v1/console/password/forgot" />

<CodePanel title="POST /v1/console/password/forgot">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/console/password/forgot' \
      -H 'content-type: application/json' \
      -d '{
      "email": "you@example.com"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    const response = await fetch('https://api.filudi.com/v1/console/password/forgot', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        email: 'you@example.com',
      }),
    });
    // No body: check response.ok.
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    response = requests.post(
        'https://api.filudi.com/v1/console/password/forgot',
        json={
            'email': 'you@example.com',
        },
    )
    response.raise_for_status()
    # No body: the status says it all.
    ```

  </Fragment>
</CodePanel>

**Email a password reset link**

Answers 202 whether the email is on an account or not. Limited per email and per IP in Redis.

Credential: none. The route is public.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string (email) | yes |  |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 202 | none | Sent, or silently not sent. |
| 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. |

<RouteHeading id="post-v1-console-password-reset" method="POST" path="/v1/console/password/reset" />

<CodePanel title="POST /v1/console/password/reset">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/console/password/reset' \
      -H 'content-type: application/json' \
      -d '{
      "token": "string",
      "password": "string"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    const response = await fetch('https://api.filudi.com/v1/console/password/reset', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        token: 'string',
        password: 'string',
      }),
    });
    // No body: check response.ok.
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    response = requests.post(
        'https://api.filudi.com/v1/console/password/reset',
        json={
            'token': 'string',
            'password': 'string',
        },
    )
    response.raise_for_status()
    # No body: the status says it all.
    ```

  </Fragment>
</CodePanel>

**Set a new password from a reset link**

Same policy as `PUT /v1/account/password`. The token is single use. Other sessions of the account stay signed in.

Credential: none. The route is public.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `token` | string | yes |  |
| `password` | [NewPassword](/docs/api/reference/schemas/#schema-newpassword) | yes | The policy of `PUT /v1/account/password`: 8 to 128 characters after NFC normalization, not equal to the email; 8 to 128 characters; write only |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 204 | none | Saved. |
| 400 | none | The password breaks the policy. |
| 404 | none | Unknown, expired or used token. |

<RouteHeading id="put-v1-console-password" method="PUT" path="/v1/console/password" />

<CodePanel title="PUT /v1/console/password">
  <Fragment slot="curl">

    ```sh
    curl -X PUT 'https://api.filudi.com/v1/console/password' \
      -b "filudi_console=$FILUDI_SESSION" \
      -H 'origin: https://developers.filudi.com' \
      -H 'content-type: application/json' \
      -d '{
      "current": "string",
      "password": "string"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // The browser sends the filudi_console cookie and the Origin header itself.
    const response = await fetch('https://api.filudi.com/v1/console/password', {
      method: 'PUT',
      credentials: 'include',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        current: 'string',
        password: 'string',
      }),
    });
    // No body: check response.ok.
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    # session: the value of the filudi_console cookie set by POST /v1/console/sign-in
    response = requests.put(
        'https://api.filudi.com/v1/console/password',
        cookies={'filudi_console': session},
        headers={'origin': 'https://developers.filudi.com'},
        json={
            'current': 'string',
            'password': 'string',
        },
    )
    response.raise_for_status()
    # No body: the status says it all.
    ```

  </Fragment>
</CodePanel>

**Change the password**

Same policy as `PUT /v1/account/password`. Other sessions stay signed in.

Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `current` | string | yes | 1 to 128 characters; write only |
| `password` | [NewPassword](/docs/api/reference/schemas/#schema-newpassword) | yes | The policy of `PUT /v1/account/password`: 8 to 128 characters after NFC normalization, not equal to the email; 8 to 128 characters; write only |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 204 | none | Saved. |
| 400 | none | The password breaks the policy. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing session, or `current` is wrong; then the body carries `details: { field: "current" }`. |

<RouteHeading id="post-v1-console-email" method="POST" path="/v1/console/email" />

<CodePanel title="POST /v1/console/email">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/console/email' \
      -b "filudi_console=$FILUDI_SESSION" \
      -H 'origin: https://developers.filudi.com' \
      -H 'content-type: application/json' \
      -d '{
      "password": "string",
      "email": "you@example.com"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // The browser sends the filudi_console cookie and the Origin header itself.
    const response = await fetch('https://api.filudi.com/v1/console/email', {
      method: 'POST',
      credentials: 'include',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        password: 'string',
        email: 'you@example.com',
      }),
    });
    // No body: check response.ok.
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    # session: the value of the filudi_console cookie set by POST /v1/console/sign-in
    response = requests.post(
        'https://api.filudi.com/v1/console/email',
        cookies={'filudi_console': session},
        headers={'origin': 'https://developers.filudi.com'},
        json={
            'password': 'string',
            'email': 'you@example.com',
        },
    )
    response.raise_for_status()
    # No body: the status says it all.
    ```

  </Fragment>
</CodePanel>

**Ask to change the email address**

A confirmation link goes to the new address. The account keeps its current email until `POST /v1/console/email/confirm`.

Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `password` | string | yes | 1 to 128 characters; write only |
| `email` | string (email) | yes |  |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 202 | none | Confirmation link sent to the new address. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing session, or the password is wrong. |
| 409 | none | The email is already on an account. |

<RouteHeading id="post-v1-console-email-confirm" method="POST" path="/v1/console/email/confirm" />

<CodePanel title="POST /v1/console/email/confirm">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/console/email/confirm' \
      -H 'content-type: application/json' \
      -d '{
      "token": "string"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    const response = await fetch('https://api.filudi.com/v1/console/email/confirm', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        token: 'string',
      }),
    });
    // No body: check response.ok.
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    response = requests.post(
        'https://api.filudi.com/v1/console/email/confirm',
        json={
            'token': 'string',
        },
    )
    response.raise_for_status()
    # No body: the status says it all.
    ```

  </Fragment>
</CodePanel>

**Confirm a new email address from its link**

Credential: none. The route is public.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `token` | string | yes |  |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 204 | none | Email changed. |
| 404 | none | Unknown, expired or used token. |
| 409 | none | The email went on another account in the meantime. |

<RouteHeading id="post-v1-console-totp" method="POST" path="/v1/console/totp" />

<CodePanel title="POST /v1/console/totp">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/console/totp' \
      -b "filudi_console=$FILUDI_SESSION" \
      -H 'origin: https://developers.filudi.com'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // The browser sends the filudi_console cookie and the Origin header itself.
    const response = await fetch('https://api.filudi.com/v1/console/totp', {
      method: 'POST',
      credentials: 'include',
    });
    const data = await response.json();
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    # session: the value of the filudi_console cookie set by POST /v1/console/sign-in
    response = requests.post(
        'https://api.filudi.com/v1/console/totp',
        cookies={'filudi_console': session},
        headers={'origin': 'https://developers.filudi.com'},
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**Start a TOTP enrolment**

Answers a new secret and its `otpauth://` URL for the authenticator app. The secret is not active until
`POST /v1/console/totp/confirm` accepts a code; starting again replaces an unconfirmed secret. Staff
without TOTP gets 403 on every `/v1/admin/*` route except `GET /v1/admin/me`.

Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead.

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [TotpEnrolment](/docs/api/reference/schemas/#schema-totpenrolment) | Secret to show once. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. |
| 409 | none | TOTP is already enabled; remove it first. |

<RouteHeading id="delete-v1-console-totp" method="DELETE" path="/v1/console/totp" />

<CodePanel title="DELETE /v1/console/totp">
  <Fragment slot="curl">

    ```sh
    curl -X DELETE 'https://api.filudi.com/v1/console/totp' \
      -b "filudi_console=$FILUDI_SESSION" \
      -H 'origin: https://developers.filudi.com' \
      -H 'content-type: application/json' \
      -d '{
      "code": "string"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // The browser sends the filudi_console cookie and the Origin header itself.
    const response = await fetch('https://api.filudi.com/v1/console/totp', {
      method: 'DELETE',
      credentials: 'include',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        code: 'string',
      }),
    });
    // No body: check response.ok.
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    # session: the value of the filudi_console cookie set by POST /v1/console/sign-in
    response = requests.delete(
        'https://api.filudi.com/v1/console/totp',
        cookies={'filudi_console': session},
        headers={'origin': 'https://developers.filudi.com'},
        json={
            'code': 'string',
        },
    )
    response.raise_for_status()
    # No body: the status says it all.
    ```

  </Fragment>
</CodePanel>

**Remove TOTP, with a current code**

Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | [TotpCode](/docs/api/reference/schemas/#schema-totpcode) | yes | Six digits, "123456" or "123 456"; pattern `^[0-9 ]{6,7}$` |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 204 | none | Removed. |
| 400 | none | Wrong code. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. |

<RouteHeading id="post-v1-console-totp-confirm" method="POST" path="/v1/console/totp/confirm" />

<CodePanel title="POST /v1/console/totp/confirm">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/console/totp/confirm' \
      -b "filudi_console=$FILUDI_SESSION" \
      -H 'origin: https://developers.filudi.com' \
      -H 'content-type: application/json' \
      -d '{
      "code": "string"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // The browser sends the filudi_console cookie and the Origin header itself.
    const response = await fetch('https://api.filudi.com/v1/console/totp/confirm', {
      method: 'POST',
      credentials: 'include',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        code: 'string',
      }),
    });
    // No body: check response.ok.
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    # session: the value of the filudi_console cookie set by POST /v1/console/sign-in
    response = requests.post(
        'https://api.filudi.com/v1/console/totp/confirm',
        cookies={'filudi_console': session},
        headers={'origin': 'https://developers.filudi.com'},
        json={
            'code': 'string',
        },
    )
    response.raise_for_status()
    # No body: the status says it all.
    ```

  </Fragment>
</CodePanel>

**Confirm the enrolment with a first code; TOTP is on from here**

Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead.

### Request body

`application/json`, required.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `code` | [TotpCode](/docs/api/reference/schemas/#schema-totpcode) | yes | Six digits, "123456" or "123 456"; pattern `^[0-9 ]{6,7}$` |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 204 | none | TOTP enabled. |
| 400 | none | Wrong code, or no enrolment started. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. |

<RouteHeading id="get-v1-console-sessions" method="GET" path="/v1/console/sessions" />

<CodePanel title="GET /v1/console/sessions">
  <Fragment slot="curl">

    ```sh
    curl 'https://api.filudi.com/v1/console/sessions' \
      -b "filudi_console=$FILUDI_SESSION"
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // The browser sends the filudi_console cookie and the Origin header itself.
    const response = await fetch('https://api.filudi.com/v1/console/sessions', {
      credentials: 'include',
    });
    const data = await response.json();
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    # session: the value of the filudi_console cookie set by POST /v1/console/sign-in
    response = requests.get(
        'https://api.filudi.com/v1/console/sessions',
        cookies={'filudi_console': session},
    )
    response.raise_for_status()
    data = response.json()
    ```

  </Fragment>
</CodePanel>

**The account's sessions, newest first**

Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead.

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 200 | [ConsoleSessionInfo](/docs/api/reference/schemas/#schema-consolesessioninfo)[] | Sessions. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. |

<RouteHeading id="delete-v1-console-sessions-sessionid" method="DELETE" path="/v1/console/sessions/{sessionId}" />

<CodePanel title="DELETE /v1/console/sessions/{sessionId}">
  <Fragment slot="curl">

    ```sh
    curl -X DELETE 'https://api.filudi.com/v1/console/sessions/00000000-0000-0000-0000-000000000000' \
      -b "filudi_console=$FILUDI_SESSION" \
      -H 'origin: https://developers.filudi.com'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // The browser sends the filudi_console cookie and the Origin header itself.
    const response = await fetch('https://api.filudi.com/v1/console/sessions/00000000-0000-0000-0000-000000000000', {
      method: 'DELETE',
      credentials: 'include',
    });
    // No body: check response.ok.
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    # session: the value of the filudi_console cookie set by POST /v1/console/sign-in
    response = requests.delete(
        'https://api.filudi.com/v1/console/sessions/00000000-0000-0000-0000-000000000000',
        cookies={'filudi_console': session},
        headers={'origin': 'https://developers.filudi.com'},
    )
    response.raise_for_status()
    # No body: the status says it all.
    ```

  </Fragment>
</CodePanel>

**Revoke one session; revoking the current one signs this browser out**

Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `sessionId` | path | string (uuid) | yes |  |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 204 | none | Revoked. |
| 401 | [Error](/docs/api/reference/schemas/#schema-error) | Missing or invalid token. |
| 404 | none | Not a session of this account. |

<RouteHeading id="post-v1-console-impersonation-end" method="POST" path="/v1/console/impersonation/end" />

<CodePanel title="POST /v1/console/impersonation/end">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/console/impersonation/end' \
      -b "filudi_console=$FILUDI_SESSION" \
      -H 'origin: https://developers.filudi.com'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    // The browser sends the filudi_console cookie and the Origin header itself.
    const response = await fetch('https://api.filudi.com/v1/console/impersonation/end', {
      method: 'POST',
      credentials: 'include',
    });
    // No body: check response.ok.
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    # session: the value of the filudi_console cookie set by POST /v1/console/sign-in
    response = requests.post(
        'https://api.filudi.com/v1/console/impersonation/end',
        cookies={'filudi_console': session},
        headers={'origin': 'https://developers.filudi.com'},
    )
    response.raise_for_status()
    # No body: the status says it all.
    ```

  </Fragment>
</CodePanel>

**End an impersonation**

Send it with `X-Filudi-Impersonation: 1`. Deletes the impersonation session and clears the `filudi_impersonation` cookie.

Credential: the console session cookie `filudi_console`, set by `POST /v1/console/sign-in`. From a browser, send `credentials: 'include'`; on a non-GET request the `Origin` header must be one of the console origins. With `X-Filudi-Impersonation: 1` the API reads the `filudi_impersonation` cookie instead.

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 204 | none | Ended. |
| 401 | none | No impersonation session. |

<RouteHeading id="post-v1-console-invitation-requests" method="POST" path="/v1/console/invitation-requests" />

<CodePanel title="POST /v1/console/invitation-requests">
  <Fragment slot="curl">

    ```sh
    curl -X POST 'https://api.filudi.com/v1/console/invitation-requests' \
      -H 'content-type: application/json' \
      -d '{
      "email": "you@example.com",
      "wants": [
        "games"
      ],
      "message": "string"
    }'
    ```

  </Fragment>
  <Fragment slot="javascript">

    ```js
    const response = await fetch('https://api.filudi.com/v1/console/invitation-requests', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        email: 'you@example.com',
        wants: ['games'],
        message: 'string',
      }),
    });
    // No body: check response.ok.
    ```

  </Fragment>
  <Fragment slot="python">

    ```py
    import requests

    response = requests.post(
        'https://api.filudi.com/v1/console/invitation-requests',
        json={
            'email': 'you@example.com',
            'wants': ['games'],
            'message': 'string',
        },
    )
    response.raise_for_status()
    # No body: the status says it all.
    ```

  </Fragment>
</CodePanel>

**Ask for an invitation, from the form on developers.filudi.com**

Stored for staff, who invites or dismisses. No email is sent. 5 requests per IP per day.

Credential: none. The route is public.

### Request body

`application/json`, required. The body is a [InvitationRequestInput](/docs/api/reference/schemas/#schema-invitationrequestinput).

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string (email) | yes | at most 254 characters |
| `wants` | array of one of `"games"`, `"portals"` | yes | What they build; 1 to 2 items; unique items |
| `message` | string | yes | Counted after trim; 1 to 500 characters |

### Responses

| Status | Body | Description |
| --- | --- | --- |
| 202 | none | Stored. |
| 400 | [Error](/docs/api/reference/schemas/#schema-error) | Validation failed. |
| 429 | [Error](/docs/api/reference/schemas/#schema-error) | Too many requests. Headers: `Retry-After`. |
| 503 | none | Redis is down; the rate limit cannot be checked. |
