--- 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