

---
openapi: 3.0.0
servers:
  - url: 'http://todo.adrift.space'
  - url: 'http://localhost:8001'
info:
  title: 'Todo-3.0 REST API'
  description: >-
    **TODO** write this
  version: '0.0.4'
  contact:
    email: hugo@lysator.liu.se
  license:
    name: Apache-2.0
    url: 'https://www.apache.org/licenses/LICENSE-2.0.html'

security:
  - bearerAuth: []

# TODO 400 if X-Todo-3.0-Role is missing, on basically all operations

# TODO summary fields are plain text, and should be short, while
# description fields are longer, and support common mark. Therefore,
# change all description tags to use `|-` instead of `>-`.

paths:

  /api/entry/{id}:
    parameters:
      - $ref: '#/components/parameters/entry-id'
      - $ref: '#/components/parameters/role'
    get:
      security: [{bearerAuth: []}]
      operationId: getEntry
      summary: >-
        Return the contents of a single entry
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EntryResponse'
          headers:
            ETag:
              required: true
              schema: {type: string}
            Last-Modified:
              required: true
              schema:
                type: string
                format: http-date
              description: >-
                Latest modification time of the given entry. Will
                always be the same value as the `updated` field in the
                response object.

        '401': {$ref: '#/components/responses/ErrorUnauthorized'}
        '403': {$ref: '#/components/responses/NoRoleForAccount'}
        '404': {$ref: '#/components/responses/ErrorNotFound'}

    put:
      operationId: putEntry
      security: [{bearerAuth: []}]
      summary: >-
        Creates or updates the target entry
      parameters:
        - name: If-Match
          in: header
          schema: {type: string}
          description: >-
            If set to `*` then this put can only update existing
            entries, and not create new ones. Other etags aren't
            supported.
        - name: If-None-Match
          in: header
          schema: {type: string}
          description: >-
            If set to `*` then this operation can only create new
            entries, and not update existing entries. Other etags
            aren't supported.
        - name: If-Modified-Since
          in: header
          schema:
            type: string
            format: http-date
          description: >-
            Requires that the entry on the server has been updated
            AFTER the specified time.
        - name: If-Unmodified-Since
          in: header
          schema:
            type: string
            format: http-date
          description: >-
            Requires that the entry on the server hasn't been modified
            since the requested time. Setting this to the local copy
            of the entries last-updated value ensures that no other
            client has written the entry in the meantime.

      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EntryRequest'

      responses:

        '201':
          description: 'Entry created'
          headers:
            ETag:
              required: true
              schema:
                type: string
        '204':
          description: 'Entry updated'
          headers:
            ETag:
              required: true
              schema:
                type: string

        '400': {$ref: '#/components/responses/ErrorBadRequest'}
        '401': {$ref: '#/components/responses/ErrorUnauthorized'}
        '403': {$ref: '#/components/responses/NoRoleForAccount'}

        '412': {$ref: '#/components/responses/ErrorPreconditionFailed'}

  /api/query:
    parameters:
      - $ref: '#/components/parameters/role'
    get:
      operationId: queryEntry
      security: [{bearerAuth: []}]
      summary: >-
        Search for entries matching criteria.

        This method also gathers all related entries, and returns
        a forest of results.
      parameters:
        - name: q
          in: query
          required: true
          description: .
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QueryOr'
        - name: include_related
          in: query
          description: >-
            Should entries related to the actual result of the querry
            also be included in the result. This includes parents,
            children, and other (explicit) dependencies.
          schema: { type: boolean }
          allowEmptyValue: true
          x-isFlag: true
        - name: group_results
          in: query
          description: >-
            The returned entries may contain parent-children relations
            between one another. If this parameter is present then
            these will be bound into a propper tree hierarchy (a list
            of trees are returned). Otherwise the result is a flat list.
          schema: { type: boolean }
          allowEmptyValue: true
          x-isFlag: true
      responses:
        '200':
          description: >-
            The matched entries, possibly accompanied with related entries.

            If `group_results` was provided, then the result will be a
            list of EntryTree objects, otherwise it will be a list of
            flat Entry objects.
          headers:
            Date:
              schema:
                type: string
                format: http-date
              description: >-
                Date when this response was generated by the server.
          content:
            application/json:
              schema:
                type: object
                required: [date, entries]
                properties:
                  date:
                    type: string
                    format: date-time
                    description: >-
                      Same information as in the `Date` header, but
                      in ISO8601 format. Repeated here because CORS
                      requests can't access the `Date` header.
                  entries:
                    type: array
                    items: {$ref: '#/components/schemas/EntryResponse'}
#                   oneOf:
#                     - type: array
#                       items:
#                         $ref: '#/components/schemas/EntryTree'
#                     - type: array
#                       items:
#                         $ref: '#/components/schemas/EntryResponse'
        '400': {$ref: '#/components/responses/ErrorBadRequest'}
        '401': {$ref: '#/components/responses/ErrorUnauthorized'}
        '403': {$ref: '#/components/responses/NoRoleForAccount'}


  /auth/login:
    # TODO move the username:password to the Authentication header?
    post:
      security: []
      operationId: login
      summary: >-
        Log in to an existing account.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password]
              properties:
                username:
                  type: string
                password:
                  type: string

      responses:
        '200': {$ref: '#/components/responses/Login'}
        '400': {$ref: '#/components/responses/ErrorBadRequest'}
        '401':
          description: >-
            Invalid username or password.

            Note that the API doesn't distinguish between these two,
            to prevent attackers from extracting registered usernames.

            Note that this *doesn't* use the comomn unauthorized
            error. This is since that error refers to the
            authorization tokens, something which we obviously don't
            have at this stage.
          content:
            text/plain: {}
              # description: >-
              #   Short description of why the login failed. Most likely
              #   incorrect username or password.

  /auth/refresh:
    post:
      operationId: refresh
      security: []
      summary: >-
        Refreshes a login token, without having to prompt for password.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refresh]
              properties:
                refresh:
                  type: string
                  description: >-
                    The refresh token returned by the last login or
                    refresh call.
      responses:
        '200': {$ref: '#/components/responses/Login'}
        '400':
          description: >-
            Either `refresh` or `account` was missing from the request.
        '401':
          description: >-
            The refresh failed. Either the user supplied bad data, or
            the refresh token has expired. In either case, the client
            should discard the existing refresh token, and start start
            a brand new authentication flow.


  /api/role:
    post:
      operationId: createRole
      security: [{bearerAuth: []}]
      summary: >-
        Create a new role.

        A role is passed in the request body, as a reference for the
        new true role.

        The id of the created role is used to guarantee that this role
        haven't been crerated on the server before. Future attempts to
        create a role with the same id (from the same account) will
        result in 409 CONFLICT (see below). This to allow an
        indempotent set, in case the client fails to store the id.

        Note that once created, a new role id is generated, and the
        client should replace all instances of the old id for the new
        one.

      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/Role'}

      responses:
        '201':
          description: >-
            A new role has been added to the account.
            Returned is the new role object (containing it's ID, and
            confirming the requested name), along with an updated
            authentication token which also is valid for the newly
            added role. This does NOT reset the expiration date of the
            token.
          content:
            application/json:
              schema:
                type: object
                required:
                  - role
                  # - jwt
                properties:
                  role: {$ref: '#/components/schemas/Role'}
                  # jwt: {type: string}

        '400': {$ref: '#/components/responses/ErrorBadRequest'}
        '401': {$ref: '#/components/responses/ErrorUnauthorized'}

        '409':
          description: >-
            The role already exists on the server. This is checked by
            the id of the posted role. This can result if the client
            attempted to create a role, but failed to store the
            response (that the role was created serverside).

            The response body is otherwise identical in format to the
            successfull request, containing the serverside role.

            This is similar to a 412 Precondition Failed which one
            might expect if setting the If-None-Match header on a
            request. The reason the roles id field is used instead of
            `If-None-Match: *` is to allow this to be robust when
            a client attempts to create a role multiple times, but
            fails to realise that the server has accepted its
            creation.

          content:
            application/json:
              schema:
                type: object
                required:
                  - role
                properties:
                  role: {$ref: '#/components/schemas/Role'}


    get:
      operationId: getRoles
      security: [{bearerAuth: []}]
      summary: >-
        Get list of roles for user
      responses:
        '200':
          description: >-
            List of all roles accessible for the currently logged in
            account. Note that this may be a different set than the
            current token is validated for (if roles have been added
            or removed, or a partial token was previously requested).
          content:
            application/json:
              schema:
                type: array
                items:
                  allOf:
                    - $ref: '#/components/schemas/Role'
                    - type: object
                      required: [creation_id]
                      properties:
                        creation_id:
                          $ref: '#/components/schemas/role-id'
                          description: >-
                            Id passed when creating the role.
                            This is relevant if the client has
                            attempted to create a role, but failed to
                            manage the response. E.g. when adding
                            server provided roles to the local set,
                            check this value against the local id of
                            each preliminary role.

            text/plain:
              schema:
                description: >-
                  Plain text list of roles, suitable to directly
                  display to the user.  Output is *not* stable, and
                  should *never* be parsed manually.
                type: string
                example: |-
                  - Role Name (#00CC00)
                    55ab9690-6bf7-4f67-abbc-49cfb82363fc
                  ...

        '401': {$ref: '#/components/responses/ErrorUnauthorized'}

  /api/role/{id}:
    parameters:
      - name: id
        schema: {$ref: '#/components/schemas/role-id'}
        required: true
        in: path

    # TODO getRole should probabyl exist for completeness

    delete:
      operationId: deleteRole
      security: [{bearerAuth: []}]
      summary: >-
        Delete a given role.
        TODO what happens with entries? What about shared roles?
      responses:
        '200':
          description: >-
            Role was successfully deleted. An updated authentication
            token is returned, identical to the one passed, except for
            the deleted role removed. Changing token to this is
            optional, but may trim some weight from the request
            headers.
#         content:
#           application/binary: {}
        '401': {$ref: '#/components/responses/ErrorUnauthorized'}
        '403': {$ref: '#/components/responses/NoRoleForAccount'}
        '404':
          $ref: '#/components/responses/ErrorNotFound'
          description: >-
            No role with that ID exists for the given account.

    put:
      operationId: updateRole
      security: [{bearerAuth: []}]
      summary: >-
        Update the fields of an existing role.

        NOTE that currently, no etag system or similar exists for
        roles, meaning that these writes WILL overwrite already
        present data.

        Unlike the `get` method, this method MAY return 403 Forbidden,
        in cases where the role is known, but the user only has read
        access to it.

      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/Role'}

      responses:
        '204':
          description: >-
            Server accepted the changes.
        '400':
          description: >-
            Either the id mentioned in the path didn't match the id
            in the body, or the request was generally malformed.
          content:
            application/json:
              schema:
                discriminator: { propertyName: error }
                oneOf:
                  - {$ref: '#/components/schemas/BAD_REQUEST'}
                  - {$ref: '#/components/schemas/ID_MISMATCH'}
        '401': {$ref: '#/components/responses/ErrorUnauthorized'}
        '403': {$ref: '#/components/responses/NoRoleForAccount'}
        '404': {$ref: '#/components/responses/ErrorNotFound'}


  /api/ping:
    get:
      operationId: ping
      summary: >-
        Echo endpoint to for checking that server is up and running
      security: []
#     requestBody:
#       content:
#         application/binary: {}
      responses:
        '200':
          description: >-
            Server is up and running
          content:
            application/json:
              schema:
                type: object
                required:
#                 - body
                  - your-ip
                properties:
#                 body:
#                   type: string
#                   description: The sent request body
                  your-ip:
                    oneOf:
                      - { type: string, format: ipv4 }
                      - { type: string, format: ipv6 }
                      - { type: string, enum: ['unknown'] }
        '401': {$ref: '#/components/responses/ErrorUnauthorized'}

  /auth/oauth/callback:
    get:
      operationId: oauthCallback
      summary: >-
        Callback the OAuth provider redirects the user to after a
        successfull authentication. The actual token is returned to
        the api by polling `/auth/oauth/token`, while this endpoints
        return value will (usually) simply be displayed to the user in
        the browser.

        The query parameters are specified in RFC 6479 (OAuth 2.0) §4.1.2.
      security: []
      parameters:
        - name: state
          required: true
          in: query
          schema: { type: string }

        - name: code
          required: false
          in: query
          schema: { type: string }

        - name: error
          required: false
          in: query
          schema:
            type: string
#           enum:
#             - invalid_request
#             - unauthorized_client
#             - access_denied
#             - unsupported_response_type
#             - invalid_scope
#             - server_error
#             - temporarily_unavailable

        - name: error_description
          required: false
          in: query
          schema: { type: string }

        - name: error_uri
          required: false
          in: query
          schema: { type: string }


      responses:
        '200':
          description: >-
            The OAuth flow succeeded .
          content:
            text/html: {}
        '400':
          description: >-
            The OAuth flow failed in some way. See the body for hints
            at what went wrong.
          content:
            text/html: {}

  /auth/oauth/token:
    get:
      operationId: oauthToken
      summary: >-
        Once the OAuth authentication has gone through, the users one
        time token for getting a true authorization token appears
        here. The user client may have to poll this multiple times,
        since it may take a while for the user to complete the
        authentication with the OAuth provider.

        Once a positive response has been gotten, the token is purged
        from the server.

        TODO this response should NEVER be cached. Can this be added here?

      security: []
      parameters:
        - name: secret
          required: true
          in: query
          schema: { type: string }
        - name: provider
          required: true
          in: query
          schema: { type: string }

      responses:
        '200':
          description: >-
            A token representing a completed oauth flow.

            Once the client has completed its authentication, and the server has been
            notified about this, then one of these tokens appear at /auth/oauth/token.

            See also frontend-common/authenticator/auth-token-extra,
            which is this type, but "parsed".
          content:
            application/json:
              schema: {$ref: '#/components/schemas/OAuthToken'}

        '404':
          description: >-
            Either the token requested is incorrect, or the token is
            not yet ready. If you expect a token to soon be ready,
            re-poll after a while.

        '500':
          description: >-
            After the OAuth callback was completed, when the server
            attempted to fetch further information about the user,
            something went wrong. See message body for possible hints.
          content:
            'text/plain': {}

  /api/authentication-methods:
    get:
      operationId: getAuthenticationMethods
      summary: >-
        Return a list of available authentication methods for this
        server. Note that this list omits the native authentication
        method.
      security: []
      responses:
        '200':
          description: >-
            .
          content:
            application/json:
              schema:
                type: array
                items: {$ref: '#/components/schemas/AuthenticationMethod'}
            text/html: {}

  /api/hello:
    get:
      operationId: getHello
      security: [{bearerAuth: []}]
      summary: >-
        Used when an application wants to force a login (probably to
        check that an account is valid), but not actually retrieve any
        resource.

        Response body is however still JSON to not confuse matters.
      responses:
        '401': {$ref: '#/components/responses/ErrorUnauthorized'}
        '200':
          description: >-
            .
          content:
            application/json:
              schema:
                type: object
                required: [msg]
                properties:
                  msg:
                    type: string
                    enum: ['hello']
            text/html: {}
            text/plain:
              schema:
                type: string
                examples:
                  hello:
                    value: hello

  /account/create:
    parameters:
      - name: 'pending-accounts'
        in: cookie
        schema:
          type: string
          description: >-
            Opaque cookie identifying the local user.
            Used by the server to return pending account
            registrations issued through this cookie.

    get:
      operationId: accountCreationPage
      security: [{bearerAuth: []}]
      summary: >-
        Get prompt for creating new accounts.
      responses:
        '200':
          description: .
          content:
            text/html: {}

    post:
      operationId: createNewAccount
      security: [{bearerAuth: []}]
      summary: >-
        Request the creation of a new account to the server.
        Note that this might not be an immidate action, to prevent
        bots creating accounts non-stop.

      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [username, password]
              properties:
                username:
                  type: string
                password:
                  type: string
                  format: password

      responses:
        '303':
          description: >-
            The request to create an account was successfull.
            If a 'pending-accounts' cookie was present in the request,
            then an updated instance of it may be present in the
            response, with an extended lifetime. Otherwise, a new
            cookie may have been issued. In either scenario, the
            cookie can be passed along to the GET request to possibly
            get a list of roles created through the cookie, and if
            they have been authorized yet.
          headers:
            Location:
              example: /account/create
              required: true
              schema:
                type: string
            Set-Cookie:
              schema:
                type: string



  /test/status/{code}:
    parameters:
      - name: code
        required: true
        in: path
        schema:
          type: integer
          enum: [ 200,  400 ]
      - $ref: '#/components/parameters/role'
    get:
      operationId: getTestStatus
      summary: >-
        Endpoint for testing purposes.
      security: [{bearerAuth: []}]
      responses:
        '200':
          description: .
        '400':
          description: .
        '401': {$ref: '#/components/responses/ErrorUnauthorized'}
        '403': {$ref: '#/components/responses/NoRoleForAccount'}


components:
  parameters:
    entry-id:
      name: id
      schema: {$ref: '#/components/schemas/entry-id'}
      required: true
      in: path

    role:
      name: X-Todo-3.0-Role
      schema: {$ref: '#/components/schemas/role-id'}
      required: true
      in: header
      description: .


  schemas:
    datetime:
      type: string
      format: date-time

    entry-id:
      type: string
      format: 'X-Todo-3.0-Entry-Id'

    role-id:
      type: string
      format: 'X-Todo-3.0-Role-Id'

    Dependency:
      type: object
      required: [id, title]
      properties:
        id: {$ref: '#/components/schemas/entry-id'}
        title:
          type: string
          description: >-
            Title of the referenced object.
            Possibly truncated if the title was extremely long.

    EntryResponse:
      type: object
      description: >-
        NOTE role is not included here, since all queries should
        already include the role, meaning that it would just echo back
        the request (times the number of matches).
      required:
        - id
        - title
        - priority
        - tags
        - depends
        - dependants
        - updated
        - isproject
        - child_preview
        - etag
      properties:
        id: {$ref: '#/components/schemas/entry-id'}
        title: {type: string}
        priority:
          type: integer
          minimum: -2
          maximum: 2
        tags:
          type: array
          items:
            $ref: "#/components/schemas/Tag"
        depends:
          type: array
          items: {$ref: '#/components/schemas/Dependency'}
        dependants:
          type: array
          items: {$ref: '#/components/schemas/Dependency'}
        updated: {$ref: '#/components/schemas/datetime'}
        isproject: {type: boolean}
        completed: {$ref: '#/components/schemas/datetime'}
        child_preview:
          type: array
          items: {$ref: '#/components/schemas/Dependency'}
        etag: {type: string}

        # Start optional fields

        due: {$ref: '#/components/schemas/datetime'}
        defer: {$ref: '#/components/schemas/datetime'}
        description: {type: string}
        partof: 
          type: object
          required: [id, title]
          properties:
            id: {$ref: '#/components/schemas/entry-id'}
            title:
              description: >-
                Unlike the regular `Dependency` type, this field is
                nullable. This since a missing dependency should still
                be representable. This can for example happen upon
                partial syncs.
              oneOf:
                - { type: 'string' }
                - { type: 'null' }

    EntryRequest:
      type: object
      required:
        - title
      properties:
        title:
          type: string
        priority:
          type: integer
          minimum: -2
          maximum: 2
        tags:
          type: array
          items:
            $ref: "#/components/schemas/Tag"
        due: {$ref: '#/components/schemas/datetime'}
        defer: {$ref: '#/components/schemas/datetime'}
        description: {type: string}
        partof: {$ref: '#/components/schemas/entry-id'}
        depends:
          type: array
          items: {$ref: '#/components/schemas/entry-id'}
          description: >-
            Complete list items this entry depends on.
            To set dependants for this entry, instead set them as
            dependencies on the other entry.
        updated:
          $ref: '#/components/schemas/datetime'
          description: >-
            Time the client claims to lats have updated this entry.
            If (according to the server), this timestamp is in the
            future, then it will be set to "now".
        isproject: {type: boolean}
        completed: {$ref: '#/components/schemas/datetime'}


    EntryTree:
      allOf:
        - $ref: '#/components/schemas/EntryResponse'
        - type: object
          required: [children]
          properties:
            children:
              type: array
              items:
                $ref: '#/components/schemas/EntryTree'

    Tag: {type: string}

    QueryAnd:
      description: >-
        An AND-clause in a query.

        Each key present in this structure adds a constraint to
        possible entries.  The empty query will match *all* entries,
        with each additional key restricting the result to the entries
        matching all other clauses, along with that clause.

        @see module level documentation.
        @see {@link "common/types".Entry}

        NOTE: certain types [^1] causes the compiler to infer a
        `never` type for the matching `*toSql` procedure. The
        workaround has been to wrap those types in 1-tuples.

        [^1]: at least `number` and `string`, but curiously not `boolean`.


      type: object
      properties:
        project:
          type: array
          items: {type: string}
          description: >-
            Requested project, by name.

            Matches any entry whose title exactly matches one of these
            entries, and is tagged `isProject`.

        tag:
          description: >-
            Matches any entry which has any of the given tags associated with it.
          type: array
          items: {type: string}
        entryId:
          $ref: '#/components/schemas/entry-id'
          description: >-
            Exactly matches an entry by id.
        completed:
          type: boolean
          description: >-
            If the entry must be complete or incomplete.

            Setting this to `true` will only give completed entries, and vice-versa.

            TODO ensure that is actually how it works.
            TODO allow date value for completed?
#       lastUpdated
        modifiedSince:
          type: string
          format: http-date
          description: >-
            Limit to entries which have been modified since the given date.

            TODO what if `entry.updated == modifiedSince`

        title:
          type: string
          description: >-
            Matches any entry whose title contains the given substring.

        isProject:
          type: boolean
          description: >-
            If set, only return entries which are marked as projects.

        maxPrio:
          type: integer
          description: >-
            Maximum (inclusive) priority to include in the result set.
            For example, if this is 0, only entries with priority 0 or
            lower are included.

        minPrio:
          type: integer
          description: >-
            Minimum (inclusive) priority to include in the result set.
            If set to 0, only entries with priority 0 or greater are
            included in the result set.

        hasDue:
          type: boolean
          description: >-
            Does the entry have a due date, at all?

            NOTE this field will probably be changed in the future, to
            one with more expressive power.

        description:
          type: string
          description: >-
            Substring match on description.

    QueryOr:
      description: >-
        Top level query specification.

        Each present `QueryAnd` will be evaluated. The sum of all
        returned entries becomes the result set, with duplicates
        removed.

      type: array
      items:
        $ref: '#/components/schemas/QueryAnd'

    Role:
      type: object
      required: [name, id, color]
      properties:
        id: {$ref: '#/components/schemas/role-id'}
        name:
          type: string
          description: >-
            Same name as in the request.
        color:
          type: string
          description: >-
            Role color.
            TODO extend documentation
        user_color:
          type: string
          description: >-
            User color override.

    Icon:
      type: object
      required: [src]
      properties:
        src:
          type: string
          description: >-
            URL (fragment) to icon identifying this login option.
            Should be viable to display at small sizes.

    AuthenticationMethod:
      allOf:
        - type: object
          required:
            - type
            - name
          properties:
            type:
              type: string
              description: >-
                Unique identifier for the authentication flow to use.
                See below for well-known enum values. Also added as a
                free-text field to make further expansion easier.
            name:
              type: string
              description: >-
                A human readable name for this method, to be presented
                to the user. For example, if authentication through
                GitHub, this could simply be "GitHub". This string
                should be insertable into something like
                `Continue with ${name}`, and similar.
            icons:
              type: array
              items: {$ref: '#/components/schemas/Icon'}

        - oneOf:
          - type: object
            required:
              - type
              - endpoint
              - params
              - provider
            properties:
              type:
                type: string
                enum: ['oauth']
              provider:
                type: string
                # TODO rewrite this description.
                # Clarify that the server provides it as a unique
                # identifier for this authentication method, and the
                # ONLY thing the client can do with it is pass it back
                # verbatim to the server to indicate that they want to
                # authenticate with this method (NOTE WHEN is this
                # passed back).
                #
                # Only put as an aside that it should preferably be
                # human readable, and match the `name` field of the
                # authentication method.
                description: >-
                  Provider of the authentication service. This is
                  later used in the backend to later fetch information
                  about the user. The client MUST treat this as an
                  opaque string, while the server SHOULD make it a
                  human readable value, preferably linked to the name
                  property of the authentication method.
              endpoint:
                type: string
                format: uri
                # TODO Either clarify when the token will appear, or
                # link to proper documentation of the OAuth control
                # flow.
                description: >-
                  URL to redirect the user to to initiate the
                  authentication flow.
              params:
                # TODO clarify where these parameters come from.
                # I believe they are provided by the OAuth provider
                # (read GitHub), and should be passed as is along the
                # chain.
                type: object
                description: >-
                  GET parameters to send alongside the url when
                  initating the authentication flow.
                # TODO what are these additional properties about?
                additionalProperties:
                  oneOf:
                    - type: string
                    - type: array
                      items: {type: string}
                      minItems: 2
                      maxItems: 2
              token_endpoint:
                type: string
                format: uri
                description: >-
                  Endpoint on API server where token will apear once
                  the user has authenticated with the third party.
                  Should be polled periodically after the user opens
                  the url sent in `endpoint`.

    AuthToken:
      type: object
      required: [token, refresh]
      properties:
        token:
          type: string
        refresh:
          type: object
          required: [payload, exp]
          properties:
            exp:
              type: string
              format: date-time
            payload:
              type: string

    OAuthToken:
      allOf:
        - {$ref: '#/components/schemas/AuthToken'}
        - type: object
          required:
            - extra
          properties:
            extra:
              type: object
              required:
                - type
                - identifier
                - displayname
              properties:
                type:
                  type: string
                  enum: ['oauth']
                identifier:
                  type: string
                  description: >-
                    Unique user id as issued by the OAuth provider.
                    This id should never change, even if the user
                    changes username at the provider.
                displayname:
                  type: string
                  description: >-
                    Current display name at the authentication provider.
                    This is non-normative, since it may change, but
                    is useful for displaying registered accounts to
                    the user.


    BAD_REQUEST:  # BadRequestGeneral: # 400
      title: BadRequest
      type: object
      required: [error]
      properties:
        error:
          type: string
          enum: ['BAD_REQUEST']
        description:
          type: string
          description: >-
            A human readable error message, giving a hint at
            what caused the error.

    ID_MISMATCH: # BadRequestIDMismatch: # 400
      type: object
      required: [error]
      properties:
        error:
          type: string
          enum: ['ID_MISMATCH']
        description:
          type: string
          description: >-
            A human readable error giving a hint to which
            parameter was incorrectly passed.

    ForbiddenGeneral: # 403
      type: object
      title: Forbidden
      required: [error]
      properties:
        error:
          type: string
          enum: ['FORBIDDEN']
        description:
          type: string
          description: >-
            A human readable error message, giving a hint at
            what caused the error.

    ForbiddenRole: # 403
      type: object
      required: [error]
      properties:
        error:
          type: string
          enum: ['ROLE FORBIDDEN']
          description: >-
            The choosen role (per 'X-Todo-3.0-Role') isn't present on the
            account. If it access elsewhere in the system is not revealed.

  responses:
    Login:
      description: >-
        Login was successfull, an authentication token (in JWT format)
        with a short lifespan will be passed, alongside a refresh
        token.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/AuthToken'}

    ErrorBadRequest:  # HTTP 400 (general)
      description: >-
        Request was badly formed, probably due to lacking required
        fields, or the provided fields being incorrectly formatted.

        TODO extend this to actually indicate what went wrong.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/BAD_REQUEST'}

    ErrorUnauthorized:  # HTTP 401
      description: >-
        The user either didn't supply a token, the given token is
        expired, or the given token has an invalid issuer.
      content:
        application/json:
          schema:
            title: Unauthorized
            type: object
            required: [type]
            properties:
              type:
                type: string
                description: |-
                  - `MISSING`: No authorization was given
                  - `EXPIRED`: The given authorization data has expired
                  - `INVALID_ISSUER`: The given authorization data is
                    not signed by this server.
                enum:
                  - MISSING
                  - EXPIRED
                  - INVALID_ISSUER
              msg:
                type: string
                description: >-
                  Optional human readable explanation of the error.

    NoRoleForAccount: # HTTP 403 (role not in account)
      description: >-
        The choosen role (per 'X-Todo-3.0-Role') isn't present on the
        account. If it access elsewhere in the system is not revealed.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ForbiddenRole'}

    ErrorForbidden:  # HTTP 403 (general)
      description: >-
        The attempted request was forbidden by the server.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ForbiddenGeneral'}

    ErrorNotFound:  # HTTP 404
      description: .
      content:
        application/json:
          schema:
            type: object
            title: NotFound
            required: [error]
            properties:
              error:
                type: string
                enum: ['NOT_FOUND']
              description:
                type: string

    ErrorPreconditionFailed:  # HTTP 412
      description: >-
        An HTTP precondition


  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

