diff --git a/Gemfile.lock b/Gemfile.lock index d85999b57..bd21b7a36 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -684,7 +684,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (3.2.5) + rack (3.2.6) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-contrib (2.5.0) @@ -699,7 +699,7 @@ GEM rack (>= 3.0.0, < 4) rack-proxy (0.7.7) rack - rack-session (2.1.1) + rack-session (2.1.2) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.1.0) diff --git a/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb b/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb index 69df99e14..7bd9ae0c3 100644 --- a/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb +++ b/app/controllers/api/v1/accounts/custom_attribute_definitions_controller.rb @@ -1,6 +1,7 @@ class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Accounts::BaseController before_action :fetch_custom_attributes_definitions, except: [:create] before_action :fetch_custom_attribute_definition, only: [:show, :update, :destroy] + before_action :check_authorization DEFAULT_ATTRIBUTE_MODEL = 'conversation_attribute'.freeze def index; end diff --git a/app/controllers/api/v1/accounts/labels_controller.rb b/app/controllers/api/v1/accounts/labels_controller.rb index 54455943b..6889d30a4 100644 --- a/app/controllers/api/v1/accounts/labels_controller.rb +++ b/app/controllers/api/v1/accounts/labels_controller.rb @@ -18,7 +18,16 @@ class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController end def destroy + label_title = @label.title + account_id = Current.account.id + label_deleted_at = Time.current + @label.destroy! + Labels::RemoveAssociationsJob.perform_later( + label_title: label_title, + account_id: account_id, + label_deleted_at: label_deleted_at + ) head :ok end diff --git a/app/controllers/devise_overrides/sessions_controller.rb b/app/controllers/devise_overrides/sessions_controller.rb index 974fb05e4..bd7bb9b44 100644 --- a/app/controllers/devise_overrides/sessions_controller.rb +++ b/app/controllers/devise_overrides/sessions_controller.rb @@ -25,6 +25,14 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController private + def render_create_error_not_confirmed + render_error( + :unauthorized, + I18n.t('devise_token_auth.sessions.not_confirmed', email: @resource.email), + error_code: 'user_not_confirmed' + ) + end + def find_user_for_authentication return nil unless params[:email].present? && params[:password].present? diff --git a/app/javascript/dashboard/api/companies.js b/app/javascript/dashboard/api/companies.js index 090b530c4..b98a59983 100644 --- a/app/javascript/dashboard/api/companies.js +++ b/app/javascript/dashboard/api/companies.js @@ -1,21 +1,12 @@ /* global axios */ import ApiClient from './ApiClient'; -export const buildCompanyParams = (page, sort) => { - let params = `page=${page}`; - if (sort) { - params = `${params}&sort=${sort}`; - } - return params; -}; - -export const buildSearchParams = (query, page, sort) => { - let params = `q=${encodeURIComponent(query)}&page=${page}`; - if (sort) { - params = `${params}&sort=${sort}`; - } - return params; -}; +const buildParams = params => + new URLSearchParams( + Object.entries(params).filter( + ([key, value]) => value !== undefined && (value !== '' || key === 'q') + ) + ).toString(); class CompanyAPI extends ApiClient { constructor() { @@ -24,14 +15,41 @@ class CompanyAPI extends ApiClient { get(params = {}) { const { page = 1, sort = 'name' } = params; - const requestURL = `${this.url}?${buildCompanyParams(page, sort)}`; + const requestURL = `${this.url}?${buildParams({ page, sort })}`; return axios.get(requestURL); } search(query = '', page = 1, sort = 'name') { - const requestURL = `${this.url}/search?${buildSearchParams(query, page, sort)}`; + const requestURL = `${this.url}/search?${buildParams({ q: query, page, sort })}`; return axios.get(requestURL); } + + listContacts(id, page = 1) { + return axios.get(`${this.url}/${id}/contacts?${buildParams({ page })}`); + } + + searchContacts(id, query = '', page = 1) { + const requestURL = `${this.url}/${id}/contacts/search?${buildParams({ q: query, page })}`; + return axios.get(requestURL); + } + + createContact(id, payload) { + return axios.post(`${this.url}/${id}/contacts`, payload); + } + + removeContact(id, contactId) { + return axios.delete(`${this.url}/${id}/contacts/${contactId}`); + } + + destroyCustomAttributes(id, customAttributes) { + return axios.post(`${this.url}/${id}/destroy_custom_attributes`, { + custom_attributes: customAttributes, + }); + } + + destroyAvatar(id) { + return axios.delete(`${this.url}/${id}/avatar`); + } } export default new CompanyAPI(); diff --git a/app/javascript/dashboard/api/specs/companies.spec.js b/app/javascript/dashboard/api/specs/companies.spec.js index 82fdc1c97..ca1d905de 100644 --- a/app/javascript/dashboard/api/specs/companies.spec.js +++ b/app/javascript/dashboard/api/specs/companies.spec.js @@ -1,7 +1,4 @@ -import companyAPI, { - buildCompanyParams, - buildSearchParams, -} from '../companies'; +import companyAPI from '../companies'; import ApiClient from '../ApiClient'; describe('#CompanyAPI', () => { @@ -9,7 +6,6 @@ describe('#CompanyAPI', () => { expect(companyAPI).toBeInstanceOf(ApiClient); expect(companyAPI).toHaveProperty('get'); expect(companyAPI).toHaveProperty('show'); - expect(companyAPI).toHaveProperty('create'); expect(companyAPI).toHaveProperty('update'); expect(companyAPI).toHaveProperty('delete'); expect(companyAPI).toHaveProperty('search'); @@ -32,111 +28,69 @@ describe('#CompanyAPI', () => { window.axios = originalAxios; }); - it('#get with default params', () => { + it('#get includes pagination and sorting params', () => { companyAPI.get({}); expect(axiosMock.get).toHaveBeenCalledWith( '/api/v1/companies?page=1&sort=name' ); }); - it('#get with page and sort params', () => { - companyAPI.get({ page: 2, sort: 'domain' }); - expect(axiosMock.get).toHaveBeenCalledWith( - '/api/v1/companies?page=2&sort=domain' - ); - }); - - it('#get with descending sort', () => { - companyAPI.get({ page: 1, sort: '-created_at' }); - expect(axiosMock.get).toHaveBeenCalledWith( - '/api/v1/companies?page=1&sort=-created_at' - ); - }); - - it('#search with query', () => { - companyAPI.search('acme', 1, 'name'); - expect(axiosMock.get).toHaveBeenCalledWith( - '/api/v1/companies/search?q=acme&page=1&sort=name' - ); - }); - - it('#search with special characters in query', () => { + it('#search encodes query params', () => { companyAPI.search('acme & co', 2, 'domain'); expect(axiosMock.get).toHaveBeenCalledWith( - '/api/v1/companies/search?q=acme%20%26%20co&page=2&sort=domain' + '/api/v1/companies/search?q=acme+%26+co&page=2&sort=domain' ); }); - it('#search with descending sort', () => { - companyAPI.search('test', 1, '-created_at'); - expect(axiosMock.get).toHaveBeenCalledWith( - '/api/v1/companies/search?q=test&page=1&sort=-created_at' - ); - }); - - it('#search with empty query', () => { + it('#search keeps empty query param for backend validation', () => { companyAPI.search('', 1, 'name'); expect(axiosMock.get).toHaveBeenCalledWith( '/api/v1/companies/search?q=&page=1&sort=name' ); }); - }); -}); -describe('#buildCompanyParams', () => { - it('returns correct string with page only', () => { - expect(buildCompanyParams(1)).toBe('page=1'); - }); - - it('returns correct string with page and sort', () => { - expect(buildCompanyParams(1, 'name')).toBe('page=1&sort=name'); - }); - - it('returns correct string with different page', () => { - expect(buildCompanyParams(3, 'domain')).toBe('page=3&sort=domain'); - }); - - it('returns correct string with descending sort', () => { - expect(buildCompanyParams(1, '-created_at')).toBe( - 'page=1&sort=-created_at' - ); - }); - - it('returns correct string without sort parameter', () => { - expect(buildCompanyParams(2, '')).toBe('page=2'); - }); -}); - -describe('#buildSearchParams', () => { - it('returns correct string with all parameters', () => { - expect(buildSearchParams('acme', 1, 'name')).toBe( - 'q=acme&page=1&sort=name' - ); - }); - - it('returns correct string with special characters', () => { - expect(buildSearchParams('acme & co', 2, 'domain')).toBe( - 'q=acme%20%26%20co&page=2&sort=domain' - ); - }); - - it('returns correct string with empty query', () => { - expect(buildSearchParams('', 1, 'name')).toBe('q=&page=1&sort=name'); - }); - - it('returns correct string without sort parameter', () => { - expect(buildSearchParams('test', 1, '')).toBe('q=test&page=1'); - }); - - it('returns correct string with descending sort', () => { - expect(buildSearchParams('company', 3, '-created_at')).toBe( - 'q=company&page=3&sort=-created_at' - ); - }); - - it('encodes special characters correctly', () => { - expect(buildSearchParams('test@example.com', 1, 'name')).toBe( - 'q=test%40example.com&page=1&sort=name' - ); + it('#destroyAvatar deletes the company avatar endpoint', () => { + companyAPI.destroyAvatar(1); + expect(axiosMock.delete).toHaveBeenCalledWith( + '/api/v1/companies/1/avatar' + ); + }); + + it('#listContacts fetches company contacts', () => { + companyAPI.listContacts(1, 2); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/companies/1/contacts?page=2' + ); + }); + + it('#searchContacts encodes contact search params', () => { + companyAPI.searchContacts(1, 'jane & co', 3); + expect(axiosMock.get).toHaveBeenCalledWith( + '/api/v1/companies/1/contacts/search?q=jane+%26+co&page=3' + ); + }); + + it('#createContact links a contact to the company', () => { + companyAPI.createContact(1, { contact_id: 2 }); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/companies/1/contacts', + { contact_id: 2 } + ); + }); + + it('#removeContact unlinks a contact from the company', () => { + companyAPI.removeContact(1, 2); + expect(axiosMock.delete).toHaveBeenCalledWith( + '/api/v1/companies/1/contacts/2' + ); + }); + + it('#destroyCustomAttributes removes company custom attributes', () => { + companyAPI.destroyCustomAttributes(1, ['plan']); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/companies/1/destroy_custom_attributes', + { custom_attributes: ['plan'] } + ); + }); }); }); diff --git a/app/javascript/dashboard/components-next/Companies/CompaniesCard/CompaniesCard.vue b/app/javascript/dashboard/components-next/Companies/CompaniesCard/CompaniesCard.vue index 4603521eb..fe4385bbb 100644 --- a/app/javascript/dashboard/components-next/Companies/CompaniesCard/CompaniesCard.vue +++ b/app/javascript/dashboard/components-next/Companies/CompaniesCard/CompaniesCard.vue @@ -1,7 +1,7 @@