Compare commits

...
Author SHA1 Message Date
Shivam Mishra b545653a93 feat: update enums 2025-03-06 20:05:59 +05:30
Shivam Mishra 0f02455224 feat: remove redact contact job 2025-03-06 20:05:59 +05:30
Shivam Mishra 1c2c8f26fc feat: update controllers to create delete request 2025-03-06 20:05:59 +05:30
Shivam Mishra 82408a8e03 feat: add delete requests table 2025-03-06 20:05:59 +05:30
Shivam MishraandGitHub c17c9b10d9 Merge branch 'develop' into feat/meta-delete-request 2025-03-06 13:02:34 +05:30
Muhsin KelothandGitHub e5d41f4b03 Merge branch 'develop' into feat/meta-delete-request 2025-03-04 21:20:12 +05:30
Shivam Mishra 2be2c9aa04 feat: throw malformed request for standard error 2025-03-04 16:32:18 +05:30
Shivam Mishra d9b86182f2 fix: default value 2025-03-04 16:30:31 +05:30
Shivam MishraandGitHub 79e46d1758 Merge branch 'develop' into feat/meta-delete-request 2025-03-04 15:21:49 +05:30
Shivam Mishra 5895d7b2bd test: redact contact job 2025-03-04 15:21:28 +05:30
Shivam Mishra bf5220e881 fix: fn names 2025-03-04 15:14:58 +05:30
Shivam Mishra 6bc29bfef2 fix: fn name 2025-03-04 15:10:25 +05:30
Shivam Mishra 9dddd1b020 refactor: move facebook redact job 2025-03-04 15:09:21 +05:30
Shivam Mishra bc294d8829 chore: add comment for response format 2025-03-04 15:02:09 +05:30
Shivam Mishra 4b243a44ee test: delete controller 2025-03-04 15:01:20 +05:30
Shivam Mishra f2961c4f8e fix: align to facebook spec 2025-03-04 15:01:07 +05:30
Shivam Mishra c4ea779759 test: confirm controller 2025-03-04 14:54:18 +05:30
Shivam Mishra cfb74b12f9 fix: function call in concern 2025-03-04 14:54:09 +05:30
Shivam Mishra 63bea5219b feat: use a shared concern 2025-03-04 14:45:15 +05:30
Shivam Mishra 74c611325f feat: don't remove inbox 2025-03-04 14:32:57 +05:30
Shivam Mishra 71ebacc46e refactor: rename meta to facebook 2025-03-03 17:53:31 +05:30
Shivam Mishra 770e395f17 feat: remove webhook route 2025-03-03 17:52:45 +05:30
Shivam Mishra 4a925619a1 refactor: render plain text 2025-03-03 17:51:09 +05:30
Shivam Mishra db537b5c86 feat: update confirmation url 2025-03-03 17:50:08 +05:30
Shivam Mishra cd1dbb67eb feat: update confirmation url 2025-03-03 17:43:48 +05:30
Shivam Mishra 114596db19 feat: move controller to facebook 2025-03-03 17:43:10 +05:30
Shivam Mishra 96a3999c8b feat: add meta deletion callback endpoint 2025-03-03 17:36:10 +05:30
10 changed files with 253 additions and 1 deletions
@@ -0,0 +1,35 @@
module FacebookConcern
extend ActiveSupport::Concern
class InvalidDigestError < StandardError; end
private
def deletion_processed?(code)
request = DeleteRequest.find_by!(confirmation_code: code)
request.complete?
rescue ActiveRecord::RecordNotFound
false
end
def parse_fb_signed_request(signed_request)
encoded_signature, payload = signed_request.split('.', 2)
decoded_signature = Base64.urlsafe_decode64(encoded_signature)
decoded_payload = JSON.parse(Base64.urlsafe_decode64(payload))
expected_signature = OpenSSL::HMAC.digest('sha256', app_secret, payload)
raise InvalidDigestError if decoded_signature != expected_signature
decoded_payload
end
def app_url_base
ENV.fetch('FRONTEND_URL', nil)
end
def app_secret
GlobalConfigService.load('FB_APP_SECRET', '')
end
end
@@ -0,0 +1,11 @@
class Facebook::ConfirmController < ApplicationController
include FacebookConcern
def show
if deletion_processed?(params[:id])
render plain: 'Data Deleted Successfully', status: :ok
else
render plain: 'Processing. If there is an issue, please contact support', status: :ok
end
end
end
@@ -0,0 +1,23 @@
class Facebook::DeleteController < ApplicationController
include FacebookConcern
def create
signed_request = params['signed_request']
payload = parse_fb_signed_request(signed_request)
id_to_process = payload['user_id']
delete_request = DeleteRequest.create(fb_id: id_to_process)
status_url = "#{app_url_base}/facebook/confirm/#{delete_request.confirmation_code}"
# IMPORTANT: Do not change the response format below.
# Facebook's Data Deletion Request system specifically expects responses in this format
# with a 'url' for status confirmation and a 'confirmation_code' field.
# See: https://developers.facebook.com/docs/development/create-an-app/app-dashboard/data-deletion-callback/#implementing
render json: { url: status_url, confirmation_code: delete_request.confirmation_code }, status: :ok
rescue InvalidDigestError
render json: { error: 'Invalid signature' }, status: :unprocessable_entity
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
render json: { error: e.message }, status: :malformed_request
end
end
+50
View File
@@ -0,0 +1,50 @@
# == Schema Information
#
# Table name: delete_requests
#
# id :bigint not null, primary key
# completed_at :datetime
# confirmation_code :string not null
# deleted_type :string
# status :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint
# deleted_id :integer
# fb_id :string not null
#
# Indexes
#
# index_delete_requests_on_account_id (account_id)
# index_delete_requests_on_confirmation_code (confirmation_code) UNIQUE
#
class DeleteRequest < ApplicationRecord
belongs_to :account, optional: true
enum :status, %w[pending complete].index_by(&:itself), default: :pending
before_create :ensure_unqiue_confirmation_code
private
def ensure_unqiue_confirmation_code
max_retries = 3
retry_count = 0
begin
self.confirmation_code = generate_confirmation_code
raise ActiveRecord::RecordNotUnique if self.class.exists?(confirmation_code: confirmation_code)
rescue ActiveRecord::RecordNotUnique
retry_count += 1
if retry_count > max_retries
# it's really really unlikely that we'll ever ever hit this case, but just in case
self.confirmation_code = generate_confirmation_code + SecureRandom.alphanumeric(3)
else
retry
end
end
end
def generate_confirmation_code
SecureRandom.uuid.delete('-')
end
end
+5
View File
@@ -453,6 +453,11 @@ Rails.application.routes.draw do
resource :callback, only: [:show]
end
namespace :facebook do
resources :delete, only: [:create]
resources :confirm, only: [:show]
end
namespace :linear do
resource :callback, only: [:show]
end
@@ -0,0 +1,17 @@
class CreateDeleteRequests < ActiveRecord::Migration[7.0]
def change
create_table :delete_requests do |t|
t.string :fb_id, null: false
t.string :status, null: false
t.string :confirmation_code, null: false
t.datetime :completed_at
t.string :deleted_type
t.integer :deleted_id
t.references :account, index: true, null: true
t.timestamps
end
add_index :delete_requests, :confirmation_code, unique: true
end
end
+15 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2025_02_28_185548) do
ActiveRecord::Schema[7.0].define(version: 2025_03_06_081627) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -639,6 +639,20 @@ ActiveRecord::Schema[7.0].define(version: 2025_02_28_185548) do
t.index ["account_id"], name: "index_data_imports_on_account_id"
end
create_table "delete_requests", force: :cascade do |t|
t.string "fb_id", null: false
t.string "status", null: false
t.string "confirmation_code", null: false
t.datetime "completed_at"
t.string "deleted_type"
t.integer "deleted_id"
t.bigint "account_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_delete_requests_on_account_id"
t.index ["confirmation_code"], name: "index_delete_requests_on_confirmation_code", unique: true
end
create_table "email_templates", force: :cascade do |t|
t.string "name", null: false
t.text "body", null: false
+3
View File
@@ -41,4 +41,7 @@ module Redis::RedisKeys
IG_MESSAGE_MUTEX = 'IG_MESSAGE_CREATE_LOCK::%<sender_id>s::%<ig_account_id>s'.freeze
SLACK_MESSAGE_MUTEX = 'SLACK_MESSAGE_LOCK::%<conversation_id>s::%<reference_id>s'.freeze
EMAIL_MESSAGE_MUTEX = 'EMAIL_CHANNEL_LOCK::%<inbox_id>s'.freeze
## Meta deletion flags
META_DELETE_PROCESSING = 'META_DELETE_PROCESSING::%<id>s'.freeze
end
@@ -0,0 +1,35 @@
require 'rails_helper'
RSpec.describe Facebook::ConfirmController do
describe 'GET #show' do
let(:user_id) { '12345' }
context 'when deletion is in progress' do
before do
redis_key = format(Redis::Alfred::META_DELETE_PROCESSING, id: user_id)
allow(Redis::Alfred).to receive(:get).with(redis_key).and_return('true')
end
it 'returns processing status' do
get :show, params: { id: user_id }
expect(response).to have_http_status(:ok)
expect(response.body).to eq('Processing')
end
end
context 'when deletion is completed' do
before do
redis_key = format(Redis::Alfred::META_DELETE_PROCESSING, id: user_id)
allow(Redis::Alfred).to receive(:get).with(redis_key).and_return(nil)
end
it 'returns success message' do
get :show, params: { id: user_id }
expect(response).to have_http_status(:ok)
expect(response.body).to eq('Data Deleted Successfully')
end
end
end
end
@@ -0,0 +1,59 @@
require 'rails_helper'
RSpec.describe Facebook::DeleteController do
describe 'POST #create' do
let(:user_id) { '12345' }
let(:app_secret) { 'test_app_secret' }
let(:frontend_url) { ENV.fetch('FRONTEND_URL', nil) }
let(:redis_key) { format(Redis::Alfred::META_DELETE_PROCESSING, id: user_id) }
before do
allow(GlobalConfigService).to receive(:load).with('FB_APP_SECRET', '').and_return(app_secret)
end
context 'with valid signed request' do
let(:payload) { { 'user_id' => user_id }.to_json }
let(:encoded_payload) { Base64.urlsafe_encode64(payload) }
let(:signature) { OpenSSL::HMAC.digest('sha256', app_secret, encoded_payload) }
let(:encoded_signature) { Base64.urlsafe_encode64(signature) }
let(:signed_request) { "#{encoded_signature}.#{encoded_payload}" }
before do
allow(Redis::Alfred).to receive(:set)
allow(Channels::Facebook::RedactContactDataJob).to receive(:perform_later)
end
it 'processes the delete request and returns status URL' do
post :create, params: { signed_request: signed_request }
expect(Redis::Alfred).to have_received(:set).with(redis_key, true)
expect(Channels::Facebook::RedactContactDataJob).to have_received(:perform_later).with(user_id)
expect(response).to have_http_status(:ok)
response_json = response.parsed_body
# NOTE: this is an important condition since this is exactly the format facebook expects
expect(response_json['url']).to eq("#{frontend_url}/facebook/confirm/#{user_id}")
expect(response_json['confirmation_code']).to eq(user_id)
end
end
context 'with invalid signed request' do
let(:invalid_signed_request) { 'invalid_signature.invalid_payload' }
before do
# Mock the Base64.urlsafe_decode64 to return valid values for testing
allow(Base64).to receive(:urlsafe_decode64).with('invalid_signature').and_return('decoded_signature')
allow(Base64).to receive(:urlsafe_decode64).with('invalid_payload').and_return('{"user_id":"12345"}')
allow(JSON).to receive(:parse).with('{"user_id":"12345"}').and_return({ 'user_id' => user_id })
allow(OpenSSL::HMAC).to receive(:digest).and_return('different_signature')
end
it 'returns an error for invalid signature' do
post :create, params: { signed_request: invalid_signed_request }
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end