diff --git a/.gitignore b/.gitignore index 7192ba9d9..629e1676e 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,10 @@ public/packs* *.swo *.un~ .jest-cache + +#VS Code files +.vscode + # ignore jetbrains IDE files .idea diff --git a/Gemfile b/Gemfile index d2f3465e2..49a8c0729 100644 --- a/Gemfile +++ b/Gemfile @@ -83,6 +83,9 @@ group :development do gem 'bullet' gem 'letter_opener' gem 'web-console' + + # used in swagger build + gem 'json_refs', git: 'https://github.com/sony-mathew/json_refs', ref: 'b6c142a' end group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index 9bb07458f..5d309e59a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,14 @@ GIT twitty (0.1.0) oauth +GIT + remote: https://github.com/sony-mathew/json_refs + revision: b6c142ae486e399d00fbc758e21a7ed63a934f61 + ref: b6c142a + specs: + json_refs (0.1.2) + hana + GEM remote: https://rubygems.org/ specs: @@ -201,6 +209,7 @@ GEM os (>= 0.9, < 2.0) signet (~> 0.12) haikunator (1.1.0) + hana (1.3.5) hashie (4.1.0) http-accept (1.7.0) http-cookie (1.0.3) @@ -498,6 +507,7 @@ DEPENDENCIES haikunator hashie jbuilder + json_refs! jwt kaminari koala diff --git a/app/controllers/swagger_controller.rb b/app/controllers/swagger_controller.rb new file mode 100644 index 000000000..c5f8c0f5b --- /dev/null +++ b/app/controllers/swagger_controller.rb @@ -0,0 +1,18 @@ +class SwaggerController < ApplicationController + def respond + if Rails.env.development? || Rails.env.test? + render inline: File.read(Rails.root.join('swagger', derived_path)) + else + head 404 + end + end + + private + + def derived_path + params[:path] ||= 'index.html' + path = params[:path] + path << ".#{params[:format]}" unless path.ends_with?(params[:format].to_s) + path + end +end diff --git a/config/routes.rb b/config/routes.rb index b1d718576..a067eb30f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -144,4 +144,8 @@ Rails.application.routes.draw do mount Sidekiq::Web, at: '/sidekiq' end # ---------------------------------------------------------------------- + + # Routes for swagger docs + get '/swagger/*path', to: 'swagger#respond' + get '/swagger', to: 'swagger#respond' end diff --git a/lib/tasks/swagger.rake b/lib/tasks/swagger.rake new file mode 100644 index 000000000..c13147aee --- /dev/null +++ b/lib/tasks/swagger.rake @@ -0,0 +1,22 @@ +namespace :swagger do + desc 'build combined swagger.json file from all the fragmented definitions and paths inside swagger folder' + task build: :environment do + require 'json_refs' + + base_path = Rails.root.join('swagger') + Dir.chdir(base_path) do + swagger_index = YAML.safe_load(File.open('index.yml')) + + final_build = JsonRefs.call( + swagger_index, + resolve_local_ref: false, + resolve_file_ref: true, + logging: true + ) + File.write('swagger.json', JSON.pretty_generate(final_build)) + puts 'Swagger build was succesful.' + puts "Generated #{base_path}/swagger.json" + puts 'Go to http://localhost:3000/swagger see the changes.' + end + end +end diff --git a/spec/controllers/swagger_controller_spec.rb b/spec/controllers/swagger_controller_spec.rb new file mode 100644 index 000000000..f68bd6e9e --- /dev/null +++ b/spec/controllers/swagger_controller_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' + +describe '/swagger', type: :request do + describe 'GET /swagger' do + it 'renders swagger index.html' do + get '/swagger' + expect(response).to have_http_status(:success) + expect(response.body).to include('redoc') + expect(response.body).to include('/swagger.json') + end + end +end diff --git a/swagger/definitions/error/bad_request.yml b/swagger/definitions/error/bad_request.yml new file mode 100644 index 000000000..b6facb361 --- /dev/null +++ b/swagger/definitions/error/bad_request.yml @@ -0,0 +1,9 @@ +title: data +type: object +properties: + description: + type: string + errors: + type: array + items: + $ref: '#/definitions/request_error' diff --git a/swagger/definitions/error/request.yml b/swagger/definitions/error/request.yml new file mode 100644 index 000000000..79b2ca4d4 --- /dev/null +++ b/swagger/definitions/error/request.yml @@ -0,0 +1,8 @@ +type: object +properties: + field: + type: string + message: + type: string + code: + type: string diff --git a/swagger/definitions/index.yml b/swagger/definitions/index.yml new file mode 100644 index 000000000..3ef07961d --- /dev/null +++ b/swagger/definitions/index.yml @@ -0,0 +1,39 @@ +# ERROR +bad_request_error: + $ref: ./error/bad_request.yml +request_error: + $ref: ./error/request.yml + +# RESOURCE +contact: + $ref: ./resource/contact.yml +conversation: + $ref: ./resource/conversation.yml + +# RESPONSE +extended_contact: + allOf: + - $ref: '#/definitions/contact' + - $ref: ./resource/extension/contact/show.yml +contact_base: + allOf: + - $ref: '#/definitions/contact' + - $ref: ./resource/extension/generic.yml +contact_list: + type: array + items: + allOf: + - $ref: '#/definitions/contact' + - $ref: ./resource/extension/generic.yml +contact_conversations: + type: array + items: + allOf: + - $ref: '#/definitions/conversation' + - $ref: ./resource/extension/contact/conversation.yml + +# REQUEST +contact_create: + $ref: ./request/contact/create.yml +contact_update: + $ref: ./request/contact/update.yml diff --git a/swagger/definitions/request/contact/create.yml b/swagger/definitions/request/contact/create.yml new file mode 100644 index 000000000..6e83a12fa --- /dev/null +++ b/swagger/definitions/request/contact/create.yml @@ -0,0 +1,6 @@ +type: object +properties: + account_id: + type: number + inbox_id: + type: number diff --git a/swagger/definitions/request/contact/update.yml b/swagger/definitions/request/contact/update.yml new file mode 100644 index 000000000..cafa35169 --- /dev/null +++ b/swagger/definitions/request/contact/update.yml @@ -0,0 +1,8 @@ +type: object +properties: + name: + type: string + email: + type: string + phone_number: + type: string diff --git a/swagger/definitions/resource/contact.yml b/swagger/definitions/resource/contact.yml new file mode 100644 index 000000000..01261239a --- /dev/null +++ b/swagger/definitions/resource/contact.yml @@ -0,0 +1,12 @@ +type: object +properties: + email: + type: string + name: + type: string + phone_number: + type: string + thumbnail: + type: string + additional_attributes: + type: object diff --git a/swagger/definitions/resource/conversation.yml b/swagger/definitions/resource/conversation.yml new file mode 100644 index 000000000..a6c8f18d6 --- /dev/null +++ b/swagger/definitions/resource/conversation.yml @@ -0,0 +1,21 @@ +type: object +properties: + display_id: + type: number + messages: + type: array + items: + type: object + inbox_id: + type: number + status: + type: string + enum: ['open', 'resolved'] + timestamp: + type: string + user_last_seen_at: + type: string + agent_last_seen_at: + type: agent_last_seen_at + unread_count: + type: number diff --git a/swagger/definitions/resource/extension/contact/conversation.yml b/swagger/definitions/resource/extension/contact/conversation.yml new file mode 100644 index 000000000..e54cb178a --- /dev/null +++ b/swagger/definitions/resource/extension/contact/conversation.yml @@ -0,0 +1,18 @@ +type: object +properties: + meta: + type: object + properties: + sender: + type: object + properties: + id: + type: number + name: + type: string + thumbnail: + type: string + channel: + type: string + assignee: + type: object diff --git a/swagger/definitions/resource/extension/contact/list.yml b/swagger/definitions/resource/extension/contact/list.yml new file mode 100644 index 000000000..0378da06e --- /dev/null +++ b/swagger/definitions/resource/extension/contact/list.yml @@ -0,0 +1,5 @@ +type: object +properties: + allOf: + - $ref: '#/definitions/contact' + - $ref: ./resource/extension/contact.yaml diff --git a/swagger/definitions/resource/extension/contact/show.yml b/swagger/definitions/resource/extension/contact/show.yml new file mode 100644 index 000000000..a4c26f306 --- /dev/null +++ b/swagger/definitions/resource/extension/contact/show.yml @@ -0,0 +1,7 @@ +type: object +properties: + id: + type: number + availability_status: + type: string + enum: ['online', 'offline'] \ No newline at end of file diff --git a/swagger/definitions/resource/extension/generic.yml b/swagger/definitions/resource/extension/generic.yml new file mode 100644 index 000000000..1817d821d --- /dev/null +++ b/swagger/definitions/resource/extension/generic.yml @@ -0,0 +1,4 @@ +type: object +properties: + id: + type: number \ No newline at end of file diff --git a/swagger/index.html b/swagger/index.html new file mode 100644 index 000000000..65ec1b81a --- /dev/null +++ b/swagger/index.html @@ -0,0 +1,23 @@ + + + + ReDoc + + + + + + + + + + + + diff --git a/swagger/index.yml b/swagger/index.yml new file mode 100644 index 000000000..44b75ca20 --- /dev/null +++ b/swagger/index.yml @@ -0,0 +1,17 @@ +swagger: 2.0 +info: + description: This is the api documentation for Chatwoot server. + version: 1.0.0 + title: Chatwoot + termsOfService: https://www.chatwoot.com/terms-of-service/ + contact: + email: support@chatwoot.com + license: + name: MIT License + url: https://opensource.org/licenses/MIT + host: app.chatwoot.com + basePath: /api/v1/ +paths: + $ref: ./paths/index.yml +definitions: + $ref: ./definitions/index.yml diff --git a/swagger/paths/contact/conversations.yml b/swagger/paths/contact/conversations.yml new file mode 100644 index 000000000..39279a449 --- /dev/null +++ b/swagger/paths/contact/conversations.yml @@ -0,0 +1,18 @@ +get: + tags: [Contact] + summary: Conversations + parameters: + - name: id + in: path + type: number + description: ID of contact + required: true + responses: + 200: + description: Success + schema: + $ref: '#/definitions/contact_conversations' + 404: + description: Contact not found + 403: + description: Access denied diff --git a/swagger/paths/contact/crud.yml b/swagger/paths/contact/crud.yml new file mode 100644 index 000000000..c15911cb4 --- /dev/null +++ b/swagger/paths/contact/crud.yml @@ -0,0 +1,42 @@ +get: + tags: [Contact] + summary: Show Contact + parameters: + - name: id + in: path + type: number + description: ID of contact + required: true + responses: + 200: + description: Success + schema: + $ref: '#/definitions/extended_contact' + 404: + description: Contact not found + 403: + description: Access denied + +put: + tags: [Contact] + summary: Update Contact + parameters: + - name: id + in: path + type: number + description: ID of the contact + required: true + - name: data + in: body + required: true + schema: + $ref: '#/definitions/contact_update' + responses: + 204: + description: Success + schema: + $ref: '#/definitions/contact_base' + 404: + description: Contact not found + 403: + description: Access denied diff --git a/swagger/paths/contact/list_create.yml b/swagger/paths/contact/list_create.yml new file mode 100644 index 000000000..4aad9d7ac --- /dev/null +++ b/swagger/paths/contact/list_create.yml @@ -0,0 +1,36 @@ +get: + tags: [Contact] + description: Listing all contacts with pagination + summary: List contacts + parameters: + - name: query_hash + in: query + type: string + responses: + 200: + description: Success + schema: + $ref: '#/definitions/contact_list' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/bad_request_error' + +post: + tags: [Contact] + description: Create a contact + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/contact_create' + responses: + 200: + description: Success + schema: + $ref: '#/definitions/extended_contact' + 400: + description: Bad Request Error + schema: + $ref: '#/definitions/bad_request_error' diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml new file mode 100644 index 000000000..54509e2e5 --- /dev/null +++ b/swagger/paths/index.yml @@ -0,0 +1,7 @@ +# Contacts +/contacts: + $ref: ./contact/list_create.yml +/contacts/{id}: + $ref: ./contact/crud.yml +/contacts/{id}/conversations: + $ref: ./contact/conversations.yml diff --git a/swagger/swagger.json b/swagger/swagger.json new file mode 100644 index 000000000..1228004b3 --- /dev/null +++ b/swagger/swagger.json @@ -0,0 +1,386 @@ +{ + "swagger": 2.0, + "info": { + "description": "This is the api documentation for Chatwoot server.", + "version": "1.0.0", + "title": "Chatwoot", + "termsOfService": "https://www.chatwoot.com/terms-of-service/", + "contact": { + "email": "support@chatwoot.com" + }, + "license": { + "name": "MIT License", + "url": "https://opensource.org/licenses/MIT" + }, + "host": "app.chatwoot.com", + "basePath": "/api/v1/" + }, + "paths": { + "/contacts": { + "get": { + "tags": [ + "Contact" + ], + "description": "Listing all contacts with pagination", + "summary": "List contacts", + "parameters": [ + { + "name": "query_hash", + "in": "query", + "type": "string" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/contact_list" + } + }, + "400": { + "description": "Bad Request Error", + "schema": { + "$ref": "#/definitions/bad_request_error" + } + } + } + }, + "post": { + "tags": [ + "Contact" + ], + "description": "Create a contact", + "parameters": [ + { + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/contact_create" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/extended_contact" + } + }, + "400": { + "description": "Bad Request Error", + "schema": { + "$ref": "#/definitions/bad_request_error" + } + } + } + } + }, + "/contacts/{id}": { + "get": { + "tags": [ + "Contact" + ], + "summary": "Show Contact", + "parameters": [ + { + "name": "id", + "in": "path", + "type": "number", + "description": "ID of contact", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/extended_contact" + } + }, + "404": { + "description": "Contact not found" + }, + "403": { + "description": "Access denied" + } + } + }, + "put": { + "tags": [ + "Contact" + ], + "summary": "Update Contact", + "parameters": [ + { + "name": "id", + "in": "path", + "type": "number", + "description": "ID of the contact", + "required": true + }, + { + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/contact_update" + } + } + ], + "responses": { + "204": { + "description": "Success", + "schema": { + "$ref": "#/definitions/contact_base" + } + }, + "404": { + "description": "Contact not found" + }, + "403": { + "description": "Access denied" + } + } + } + }, + "/contacts/{id}/conversations": { + "get": { + "tags": [ + "Contact" + ], + "summary": "Conversations", + "parameters": [ + { + "name": "id", + "in": "path", + "type": "number", + "description": "ID of contact", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/contact_conversations" + } + }, + "404": { + "description": "Contact not found" + }, + "403": { + "description": "Access denied" + } + } + } + } + }, + "definitions": { + "bad_request_error": { + "title": "data", + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/definitions/request_error" + } + } + } + }, + "request_error": { + "type": "object", + "properties": { + "field": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + } + }, + "contact": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "thumbnail": { + "type": "string" + }, + "additional_attributes": { + "type": "object" + } + } + }, + "conversation": { + "type": "object", + "properties": { + "display_id": { + "type": "number" + }, + "messages": { + "type": "array", + "items": { + "type": "object" + } + }, + "inbox_id": { + "type": "number" + }, + "status": { + "type": "string", + "enum": [ + "open", + "resolved" + ] + }, + "timestamp": { + "type": "string" + }, + "user_last_seen_at": { + "type": "string" + }, + "agent_last_seen_at": { + "type": "agent_last_seen_at" + }, + "unread_count": { + "type": "number" + } + } + }, + "extended_contact": { + "allOf": [ + { + "$ref": "#/definitions/contact" + }, + { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "availability_status": { + "type": "string", + "enum": [ + "online", + "offline" + ] + } + } + } + ] + }, + "contact_base": { + "allOf": [ + { + "$ref": "#/definitions/contact" + }, + { + "type": "object", + "properties": { + "id": { + "type": "number" + } + } + } + ] + }, + "contact_list": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/contact" + }, + { + "type": "object", + "properties": { + "id": { + "type": "number" + } + } + } + ] + } + }, + "contact_conversations": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/conversation" + }, + { + "type": "object", + "properties": { + "meta": { + "type": "object", + "properties": { + "sender": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + }, + "thumbnail": { + "type": "string" + }, + "channel": { + "type": "string" + } + } + }, + "assignee": { + "type": "object" + } + } + } + } + } + ] + } + }, + "contact_create": { + "type": "object", + "properties": { + "account_id": { + "type": "number" + }, + "inbox_id": { + "type": "number" + } + } + }, + "contact_update": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "phone_number": { + "type": "string" + } + } + } + } +} \ No newline at end of file