$schema: https://spec.openapis.org/oas/3.1/schema/2022-10-07 openapi: 3.1.1 info: title: Forms Portal API version: 0.1.0 summary: API Documentation for the Forms Portal contact: name: OSDG servers: - url: /api description: Production tags: - name: Authentication description: User authentication via CAS and session management - name: Forms description: Form lifecycle management - name: Responses description: Form response submission and retrieval - name: Permissions description: Role-based access control for forms - name: Groups description: User group management for bulk permission assignments - name: Comments description: Collaborative commenting on forms - name: Users description: User profile operations security: - cookieAuth: [] paths: /auth/login: get: tags: [Authentication] summary: Initiate CAS login description: Redirects the user's browser to the Central Authentication System (CAS) login page. operationId: initiateLogin responses: '302': description: Redirect to CAS login page. headers: Location: schema: type: string format: uri '500': $ref: '#/components/responses/InternalServerError' /auth/login/callback: get: tags: [Authentication] summary: Handle CAS callback description: Processes the callback from CAS after successful authentication, validates the ticket, and establishes a user session. operationId: handleCallback parameters: - name: ticket in: query required: true description: CAS service ticket provided by the authentication server. schema: type: string responses: '302': description: Redirect to the application frontend. headers: Location: schema: type: string format: uri Set-Cookie: description: Sets the session cookie. schema: type: string '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalServerError' /auth/logout: get: tags: [Authentication] summary: Logout user description: Invalidates the session and redirects the user to the CAS logout page. Should be called in the same manner as the login endpoint. operationId: logoutUser responses: '204': description: Successfully logged out. headers: Set-Cookie: description: Instructs the browser to clear the session cookie. schema: type: string '401': $ref: '#/components/responses/Unauthorized' /auth/info: get: tags: [Authentication] summary: Get current user profile description: Returns the profile information of the currently authenticated user. operationId: getCurrentUser responses: '200': description: Current user profile. content: application/json: schema: $ref: '#/components/schemas/User' '401': $ref: '#/components/responses/Unauthorized' /users/{userId}: parameters: - $ref: '#/components/parameters/userId' get: tags: [Users] summary: Get user profile info by ID description: Retrieves a user's name, email and handle from their ID. operationId: getUser responses: '200': description: The user's profile. content: application/json: schema: $ref: '#/components/schemas/User' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' /forms: get: tags: [Forms] summary: List forms description: Retrieves forms accessible to the current user, with support for filtering, sorting, and pagination. operationId: listForms parameters: - name: title in: query description: The form title. schema: type: string - name: owner in: query description: The owner's email. schema: type: string format: email - name: role in: query schema: $ref: '#/components/schemas/PermissionRole' - name: sort in: query description: Sort criteria. schema: type: string enum: [modified, title] default: modified - name: order in: query schema: type: string enum: [asc, desc] default: desc - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 20 - name: offset in: query schema: type: integer minimum: 0 default: 0 responses: '200': description: A paginated list of forms. content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/Form' pagination: $ref: '#/components/schemas/Pagination' '401': $ref: '#/components/responses/Unauthorized' '422': $ref: '#/components/responses/UnprocessableEntity' post: tags: [Forms] summary: Create form description: Creates a new form with the authenticated user as the owner. operationId: createForm requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/FormCreate' responses: '201': description: Form created successfully. content: application/json: schema: $ref: '#/components/schemas/Form' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' /forms/{handle}/{slug}: parameters: - $ref: '#/components/parameters/handle' - $ref: '#/components/parameters/slug' get: tags: [Forms] summary: Resolve form description: Resolves a user-friendly URL path (`/handle/slug`) to a full form object. operationId: resolveForm responses: '200': description: Form resolved successfully. content: application/json: schema: $ref: '#/components/schemas/Form' '403': $ref: '#/components/responses/Forbidden' /forms/{formId}: parameters: - $ref: '#/components/parameters/formId' get: tags: [Forms] summary: Get form by ID description: Retrieves a form's complete definition. Requires VIEW permission or higher. operationId: getForm responses: '200': description: Form details. content: application/json: schema: $ref: '#/components/schemas/Form' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' patch: tags: [Forms] summary: Update form description: Updates a form's definition. Requires EDIT permission or higher. operationId: updateForm requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/FormUpdate' responses: '200': description: Form updated successfully. content: application/json: schema: $ref: '#/components/schemas/Form' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/UnprocessableEntity' delete: tags: [Forms] summary: Delete form description: Permanently deletes a form and all associated data. This action cannot be undone. Requires MANAGE permission. operationId: deleteForm responses: '204': description: Form deleted successfully. '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /responses/saved: get: tags: [Responses] summary: List saved responses description: Retrieves all the saved responses for the logged in user. operationId: listSavedResponses parameters: - name: title in: query description: The form title. schema: type: string - name: status in: query schema: $ref: '#/components/schemas/ResponseStatus' - name: sort in: query description: Sort criteria. schema: type: string enum: [started, submitted, edited] default: submitted - name: order in: query schema: type: string enum: [asc, desc] default: desc - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 20 - name: offset in: query schema: type: integer minimum: 0 default: 0 responses: '200': description: List of saved responses. content: application/json: schema: type: array items: $ref: '#/components/schemas/Response' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /forms/{formId}/responses: parameters: - $ref: '#/components/parameters/formId' get: tags: [Responses] summary: List form responses description: Retrieves all completed and in-progress responses for a form. Requires ANALYZE permission or ownership of responses. operationId: listResponses parameters: - name: status in: query schema: $ref: '#/components/schemas/ResponseStatus' default: completed - name: sort in: query description: Sort criteria. schema: type: string enum: [started, submitted, edited] default: submitted - name: order in: query schema: type: string enum: [asc, desc] default: desc - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 20 - name: offset in: query schema: type: integer minimum: 0 default: 0 responses: '200': description: List of form responses. content: application/json: schema: type: array items: $ref: '#/components/schemas/Response' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' post: tags: [Responses] summary: Start response description: Initiates a new response session for the form. Requires RESPOND permission. operationId: startResponse responses: '201': description: Response session created. content: application/json: schema: $ref: '#/components/schemas/Response' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /forms/{formId}/responses/{responseId}: parameters: - $ref: '#/components/parameters/formId' - $ref: '#/components/parameters/responseId' get: tags: [Responses] summary: Get response details description: Retrieves a specific response. Requires ANALYZE permission or ownership of the response. operationId: getResponse responses: '200': description: Response details. content: application/json: schema: $ref: '#/components/schemas/Response' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /forms/{formId}/responses/{responseId}/answers: parameters: - $ref: '#/components/parameters/formId' - $ref: '#/components/parameters/responseId' get: tags: [Responses] summary: Get answers description: Retrieves all the answers submitted as part of a specific response. Requires ANALYZE permission or ownership of the response. operationId: getAnswers responses: '200': description: Response details. content: application/json: schema: type: array items: $ref: '#/components/schemas/Answer' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' put: tags: [Responses] summary: Save answer description: Creates or updates an answer for a specific question within a response. Requires ownership of response. operationId: saveAnswer requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AnswerUpsert' responses: '200': description: Answer saved successfully. content: application/json: schema: $ref: '#/components/schemas/Answer' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/UnprocessableEntity' /forms/{formId}/responses/{responseId}/submit: parameters: - $ref: '#/components/parameters/formId' - $ref: '#/components/parameters/responseId' post: tags: [Responses] summary: Submit response description: Finalizes a response by changing its status to 'completed'. Requires ownership of response. operationId: submitResponse requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ResponseSubmit' responses: '200': description: Response submitted successfully. content: application/json: schema: $ref: '#/components/schemas/Response' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /forms/{formId}/permissions: parameters: - $ref: '#/components/parameters/formId' get: tags: [Permissions] summary: List form permissions description: Retrieves all active permission grants for a form. Requires MANAGE permission. operationId: listPermissions responses: '200': description: List of permission grants. content: application/json: schema: type: array items: $ref: '#/components/schemas/Permission' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' post: tags: [Permissions] summary: Grant permission description: Grants a role on a form to a user or a group. Requires MANAGE permission. operationId: grantPermission requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PermissionCreate' responses: '201': description: Permission granted successfully. content: application/json: schema: $ref: '#/components/schemas/Permission' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' /forms/{formId}/permissions/{permissionId}: parameters: - $ref: '#/components/parameters/formId' - $ref: '#/components/parameters/permissionId' delete: tags: [Permissions] summary: Revoke permission description: Removes a permission grant from a user or group. Requires MANAGE permission. operationId: revokePermission responses: '204': description: Permission revoked successfully. '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /groups: get: tags: [Groups] summary: List groups description: Retrieves all groups owned by the current user. operationId: listGroups parameters: - name: owner in: query description: The owner's email. schema: type: string format: email - name: type in: query schema: $ref: '#/components/schemas/GroupType' - name: sort in: query description: Sort criteria. schema: type: string enum: [created, name, type] default: created - name: order in: query schema: type: string enum: [asc, desc] default: desc - name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 20 - name: offset in: query schema: type: integer minimum: 0 default: 0 responses: '200': description: A paginated list of groups. content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/Group' pagination: $ref: '#/components/schemas/Pagination' '401': $ref: '#/components/responses/Unauthorized' post: tags: [Groups] summary: Create group description: Creates a new user group with the current user as the owner. operationId: createGroup requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/GroupCreate' responses: '201': description: Group created successfully. content: application/json: schema: $ref: '#/components/schemas/Group' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' /groups/{groupId}: parameters: - $ref: '#/components/parameters/groupId' get: tags: [Groups] summary: Get group details description: Retrieves detailed information about a specific group. Requires ownership. operationId: getGroup responses: '200': description: Group details. content: application/json: schema: $ref: '#/components/schemas/Group' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' patch: tags: [Groups] summary: Update group description: Updates a group's name or description. Requires ownership. operationId: updateGroup requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/GroupUpdate' responses: '200': description: Group updated successfully. content: application/json: schema: $ref: '#/components/schemas/Group' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/UnprocessableEntity' delete: tags: [Groups] summary: Delete group description: Permanently deletes a group. Requires ownership. operationId: deleteGroup responses: '204': description: Group deleted successfully. '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /groups/{groupId}/domain: parameters: - $ref: '#/components/parameters/groupId' put: tags: [Groups] summary: Update group domain description: Updates the domain for a group of 'DOMAIN' type. Requires ownership. operationId: updateGroupDomain requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/GroupDomainUpdate' responses: '204': description: Group domain updated successfully. '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/UnprocessableEntity' /groups/{groupId}/members: parameters: - $ref: '#/components/parameters/groupId' post: tags: [Groups] summary: Add group member description: Adds a user to a 'LIST' type group. Requires group ownership. operationId: addGroupMember requestBody: required: true content: application/json: schema: type: object required: - email properties: email: type: string format: email responses: '204': description: Member added successfully. '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/UnprocessableEntity' /groups/{groupId}/members/{userId}: parameters: - $ref: '#/components/parameters/groupId' - $ref: '#/components/parameters/userId' delete: tags: [Groups] summary: Remove group member description: Removes a user from a 'LIST' type group. Requires group ownership. operationId: removeGroupMember responses: '204': description: Member removed successfully. '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /forms/{formId}/comments: parameters: - $ref: '#/components/parameters/formId' get: tags: [Comments] summary: List form comments description: Retrieves all comments for a form. Requires COMMENT permission. operationId: listComments responses: '200': description: List of form comments. content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/Comment' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' post: tags: [Comments] summary: Create comment description: Adds a new comment to a form element or as a reply. Requires COMMENT permission. operationId: createComment requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CommentCreate' responses: '201': description: Comment created successfully. content: application/json: schema: $ref: '#/components/schemas/Comment' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/UnprocessableEntity' /forms/{formId}/comments/{commentId}: parameters: - $ref: '#/components/parameters/formId' - $ref: '#/components/parameters/commentId' patch: tags: [Comments] summary: Update comment description: Updates the body or state of a comment. Requires ownership. operationId: updateComment requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CommentUpdate' responses: '200': description: Comment updated successfully. content: application/json: schema: $ref: '#/components/schemas/Comment' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/UnprocessableEntity' delete: tags: [Comments] summary: Delete comment description: Deletes a comment. Requires ownership or MANAGE permission on the form. operationId: deleteComment responses: '204': description: Comment deleted successfully. '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' components: securitySchemes: cookieAuth: type: cookie name: session parameters: handle: name: handle in: path required: true schema: type: string slug: name: slug in: path required: true schema: type: string formId: name: formId in: path required: true schema: type: string format: ulid responseId: name: responseId in: path required: true schema: type: string permissionId: name: permissionId in: path required: true schema: type: string format: ulid groupId: name: groupId in: path required: true schema: type: string format: ulid userId: name: userId in: path required: true schema: type: string format: ulid commentId: name: commentId in: path required: true schema: type: string format: ulid responses: BadRequest: description: Bad Request content: application/json: schema: $ref: '#/components/schemas/Error' Unauthorized: description: Unauthorized content: application/json: schema: $ref: '#/components/schemas/Error' Forbidden: description: Forbidden content: application/json: schema: $ref: '#/components/schemas/Error' NotFound: description: Not Found content: application/json: schema: $ref: '#/components/schemas/Error' Conflict: description: Conflict content: application/json: schema: $ref: '#/components/schemas/Error' UnprocessableEntity: description: Unprocessable Entity content: application/json: schema: $ref: '#/components/schemas/ValidationError' InternalServerError: description: Internal Server Error content: application/json: schema: $ref: '#/components/schemas/Error' schemas: PermissionRole: type: string enum: [view, respond, comment, analyze, edit, manage] GroupType: type: string enum: [domain, list] ResponseStatus: type: string enum: [draft, completed, edited] CommentState: type: string enum: [hidden, visible] User: type: object required: - id - handle - name - email properties: id: type: string format: ulid handle: type: string name: type: string email: type: string format: email Form: type: object required: - id - owner - title - slug - structure - live properties: id: type: string format: ulid owner: type: string format: ulid title: type: string slug: type: string description: type: string nullable: true structure: type: string format: kdl live: type: boolean default: false opens: type: string format: date-time nullable: true closes: type: string format: date-time nullable: true anonymous: type: boolean default: false max_responses: type: integer nullable: true individual_limit: type: integer minimum: 1 default: 1 editable_responses: type: boolean default: false FormCreate: type: object required: - title - slug - structure properties: title: type: string slug: type: string description: type: string nullable: true structure: type: string format: kdl live: type: boolean default: false opens: type: string format: date-time nullable: true closes: type: string format: date-time nullable: true anonymous: type: boolean max_responses: type: integer nullable: true individual_limit: type: integer minimum: 1 default: 1 editable_responses: type: boolean default: false FormUpdate: type: object properties: title: type: string slug: type: string description: type: string nullable: true structure: type: string format: kdl live: type: boolean opens: type: string format: date-time nullable: true closes: type: string format: date-time nullable: true anonymous: type: boolean max_responses: type: integer nullable: true individual_limit: type: integer minimum: 1 editable_responses: type: boolean Group: type: object required: - id - owner - name - type properties: id: type: string format: ulid owner: type: string format: ulid name: type: string description: type: string nullable: true type: $ref: '#/components/schemas/GroupType' domain: type: string format: fqdn description: Present if type is 'domain'. members: type: array items: type: string format: email description: Array of user emails. Present if type is 'list'. GroupCreate: type: object required: - name - type properties: name: type: string description: type: string nullable: true type: $ref: '#/components/schemas/GroupType' domain: type: string format: fqdn description: Required if type is 'domain'. members: type: array items: type: string format: email description: Array of user emails. Only applicable if type is 'list'. GroupUpdate: type: object properties: name: type: string description: type: string nullable: true GroupDomainUpdate: type: object required: - domain properties: domain: type: string format: fqdn Permission: type: object required: - id - form - role properties: id: type: string format: ulid form: type: string format: ulid role: $ref: '#/components/schemas/PermissionRole' user: type: string format: ulid nullable: true group: type: string format: ulid nullable: true PermissionCreate: type: object required: - role oneOf: - required: [user] - required: [group] properties: role: $ref: '#/components/schemas/PermissionRole' user: type: string format: email group: type: string format: ulid Response: type: object required: - id - form - status - started properties: id: type: string form: type: string format: ulid respondent: type: string format: ulid nullable: true status: $ref: '#/components/schemas/ResponseStatus' started: type: string format: date-time submitted: type: string format: date-time nullable: true edited: type: string format: date-time nullable: true Answer: type: object required: - id - response - question - value - submitted - modified properties: id: type: string format: ulid response: type: string question: type: string value: type: object additionalProperties: true submitted: type: string format: date-time modified: type: string format: date-time AnswerUpsert: type: object required: - question - value properties: question: type: string value: type: string description: This string must contain the answer, JSON.stringify(...)-ed. ResponseSubmit: type: object required: - save properties: save: type: boolean Comment: type: object required: - id - form - user - element - body - modified properties: id: type: string format: ulid form: type: string format: ulid user: type: string format: ulid body: type: string state: $ref: '#/components/schemas/CommentState' element: type: string parent: type: string format: ulid nullable: true modified: type: string format: date-time CommentCreate: type: object required: - element - body properties: element: type: string body: type: string parent: type: string format: ulid nullable: true CommentUpdate: type: object properties: body: type: string state: $ref: '#/components/schemas/CommentState' Error: type: object required: - code - message properties: code: type: string message: type: string ValidationError: type: object required: - code - message - errors properties: code: type: string message: type: string errors: type: array items: type: object properties: field: type: string message: type: string Pagination: type: object required: - offset - limit - total properties: offset: type: integer limit: type: integer total: type: integer