class HelpdeskSearchController < ApplicationController unloadable accept_api_auth :ticket_by_issue, :issues_by_contact, :messages_by_issue, :contact_timeline before_filter :require_login def usage # Human/browser probes often stop at /helpdesk_search/issues. Return a # machine-readable usage response instead of letting Rails raise a route # exception that looks like an application failure in production.log. render :json => { :helpdesk_search => { :ticket_by_issue => '/helpdesk_search/issues/:issue_id/ticket', :ticket_by_issue_alias => '/helpdesk_search/issues/:issue_id', :issues_by_contact => '/helpdesk_search/contacts/:contact_id/issues', :messages_by_issue => '/helpdesk_search/issues/:issue_id/messages', :contact_timeline => '/helpdesk_search/contacts/:contact_id/timeline' } }, :status => :bad_request end def ticket_by_issue issue = Issue.find(params[:issue_id]) return unless authorize_helpdesk_project!(issue.project) ticket = HelpdeskTicket.where(:issue_id => issue.id).first unless ticket render_404 return end render :json => {:helpdesk_ticket => serialize_ticket(ticket)} rescue ActiveRecord::RecordNotFound render_404 end def issues_by_contact contact = Contact.find(params[:contact_id]) tickets = HelpdeskTicket. includes(:issue => [:project, :status, :tracker, :assigned_to]). where(:contact_id => contact.id). order("#{HelpdeskTicket.table_name}.ticket_date DESC"). limit(api_limit) render :json => { :contact_id => contact.id, :issues => tickets.map { |ticket| serialize_ticket_issue(ticket) }.compact } rescue ActiveRecord::RecordNotFound render_404 end def messages_by_issue issue = Issue.find(params[:issue_id]) return unless authorize_helpdesk_project!(issue.project) messages = JournalMessage. includes(:contact, :journal). joins(:journal). where(:journals => {:journalized_type => 'Issue', :journalized_id => issue.id}). order("#{JournalMessage.table_name}.message_date ASC"). limit(api_limit) render :json => { :issue_id => issue.id, :journal_messages => messages.map { |message| serialize_journal_message(message) } } rescue ActiveRecord::RecordNotFound render_404 end def contact_timeline contact = Contact.find(params[:contact_id]) tickets = HelpdeskTicket. includes(:issue => [:project, :status, :tracker]). where(:contact_id => contact.id). order("#{HelpdeskTicket.table_name}.ticket_date DESC"). limit(api_limit) messages = JournalMessage. includes(:contact, :journal). where(:contact_id => contact.id). order("#{JournalMessage.table_name}.message_date DESC"). limit(api_limit) events = [] tickets.each do |ticket| next unless visible_issue?(ticket.issue) events << serialize_ticket(ticket).merge(:type => 'helpdesk_ticket', :date => iso8601(ticket.ticket_date)) end messages.each do |message| issue = message_issue(message) next unless visible_issue?(issue) events << serialize_journal_message(message).merge(:type => 'journal_message', :date => iso8601(message.message_date)) end events.sort_by! { |event| event[:date].to_s } events.reverse! render :json => { :contact_id => contact.id, :timeline => events.first(api_limit) } rescue ActiveRecord::RecordNotFound render_404 end private def authorize_helpdesk_project!(project) # Search endpoints expose customer/email context, so they reuse the native # per-project helpdesk visibility permission instead of adding a new role. return true if project && User.current.allowed_to?(:view_helpdesk_tickets, project) deny_access false end def visible_issue?(issue) issue && issue.project && User.current.allowed_to?(:view_helpdesk_tickets, issue.project) end def api_limit requested_limit = params[:limit].present? ? params[:limit].to_i : 100 [[requested_limit, 1].max, 200].min end def serialize_ticket_issue(ticket) issue = ticket.issue return nil unless visible_issue?(issue) serialize_ticket(ticket).merge( :issue => { :id => issue.id, :project_id => issue.project_id, :tracker_id => issue.tracker_id, :tracker_name => issue.tracker.try(:name), :status_id => issue.status_id, :status_name => issue.status.try(:name), :assigned_to_id => issue.assigned_to_id, :assigned_to_name => issue.assigned_to.try(:name), :subject => issue.subject, :created_on => iso8601(issue.created_on), :updated_on => iso8601(issue.updated_on) } ) end def serialize_ticket(ticket) { :id => ticket.id, :issue_id => ticket.issue_id, :contact_id => ticket.contact_id, :message_id => ticket.message_id, :source => ticket.source, :is_incoming => ticket.is_incoming?, :from_address => ticket.from_address, :to_address => ticket.to_address, :cc_address => ticket.cc_address, :ticket_date => iso8601(ticket.ticket_date) } end def serialize_journal_message(message) journal = message.journal issue = message_issue(message) # Keep this API metadata-only. The external indexer can fetch journal.notes # with its own read policy; this endpoint should not leak message bodies, # private notes, attachments, or BCC addresses by accident. { :id => message.id, :journal_id => message.journal_id, :issue_id => issue.try(:id), :project_id => issue.try(:project_id), :contact_id => message.contact_id, :message_id => message.message_id, :source => message.source, :is_incoming => message.is_incoming?, :from_address => message.from_address, :to_address => message.to_address, :cc_address => message.cc_address, :has_bcc_address => message.bcc_address.present?, :message_date => iso8601(message.message_date), :journal_user_id => journal.try(:user_id), :journal_private_notes => journal.try(:private_notes?), :journal_has_notes => journal.try(:notes).present? } end def message_issue(message) journal = message.journal return nil unless journal journal.try(:issue) || journal.try(:journalized) end def iso8601(value) value.try(:utc).try(:iso8601) end end