Merge branch 'develop' into feat/app-store-reviews

This commit is contained in:
Muhsin Keloth
2026-06-02 21:13:24 +04:00
committed by GitHub
729 changed files with 20146 additions and 2805 deletions
@@ -1,7 +1,7 @@
class Api::V1::Accounts::Articles::BulkActionsController < Api::V1::Accounts::BaseController
before_action :portal
before_action :check_authorization
before_action :set_articles, only: [:update_status, :delete_articles]
before_action :set_articles, only: [:update_status, :update_category, :delete_articles]
def translate
head :not_implemented
@@ -19,6 +19,18 @@ class Api::V1::Accounts::Articles::BulkActionsController < Api::V1::Accounts::Ba
render_could_not_create_error(e.message)
end
def update_category
return render_could_not_create_error(I18n.t('portals.articles.no_articles_found')) if @articles.none?
return render_could_not_create_error(I18n.t('portals.articles.category_not_found')) unless category_valid?
ActiveRecord::Base.transaction do
@articles.find_each { |article| article.update!(category_id: params[:category_id]) }
end
head :ok
rescue ActiveRecord::RecordInvalid => e
render_could_not_create_error(e.message)
end
def delete_articles
return render_could_not_create_error(I18n.t('portals.articles.no_articles_found')) if @articles.none?
@@ -39,5 +51,9 @@ class Api::V1::Accounts::Articles::BulkActionsController < Api::V1::Accounts::Ba
def set_articles
@articles = @portal.articles.where(id: params[:ids])
end
def category_valid?
@portal.categories.exists?(id: params[:category_id])
end
end
Api::V1::Accounts::Articles::BulkActionsController.prepend_mod_with('Api::V1::Accounts::Articles::BulkActionsController')
@@ -11,7 +11,7 @@ class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts
enable_fb_login: '0',
force_authentication: '1',
response_type: 'code',
state: generate_instagram_token(Current.account.id)
state: generate_instagram_token(Current.account.id, params[:return_to])
}
)
if redirect_url
@@ -8,7 +8,15 @@ class Api::V1::Accounts::OauthAuthorizationController < Api::V1::Accounts::BaseC
end
def state
Current.account.to_sgid(expires_in: 15.minutes).to_s
# The sgid purpose doubles as a return hint: onboarding tags it so the callback
# can route the user back to inbox setup. The purpose is part of the signed
# payload (tamper-proof), and a non-onboarding request keeps the default
# purpose, leaving callers like Notion byte-identical.
Current.account.to_sgid(expires_in: 15.minutes, for: state_purpose).to_s
end
def state_purpose
params[:return_to] == 'onboarding' ? 'onboarding' : 'default'
end
def base_url
@@ -0,0 +1,36 @@
class Api::V1::Accounts::OnboardingsController < Api::V1::Accounts::BaseController
before_action :check_admin_authorization?
def update
@account = Current.account
finalize = finalizing_account_details?
@account.assign_attributes(account_params)
@account.custom_attributes.merge!(custom_attributes_params)
@account.custom_attributes.delete('onboarding_step') if finalize
@account.save!
# TODO: re-enable when the help center generation UI is ready to surface progress
# Onboarding::HelpCenterCreationService.new(@account, Current.user).perform if finalize && website.present?
render 'api/v1/accounts/update', format: :json
end
private
def finalizing_account_details?
@account.custom_attributes['onboarding_step'] == 'account_details'
end
def website
custom_attributes_params[:website]
end
def account_params
params.permit(:name, :locale)
end
def custom_attributes_params
params.permit(:industry, :company_size, :timezone, :referral_source, :user_role, :website)
end
end
@@ -3,7 +3,7 @@ class Api::V1::Accounts::Tiktok::AuthorizationsController < Api::V1::Accounts::O
def create
redirect_url = Tiktok::AuthClient.authorize_url(
state: generate_tiktok_token(Current.account.id)
state: generate_tiktok_token(Current.account.id, params[:return_to])
)
if redirect_url
@@ -58,7 +58,6 @@ class Api::V1::AccountsController < Api::BaseController
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email))
@account.custom_attributes.merge!(custom_attributes_params)
@account.settings.merge!(settings_params)
@account.custom_attributes.delete('onboarding_step') if @account.custom_attributes['onboarding_step'] == 'account_details'
@account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update'
@account.save!
end
@@ -28,6 +28,8 @@ class Instagram::CallbacksController < ApplicationController
@long_lived_token_response = exchange_for_long_lived_token(@response.token)
inbox, already_exists = find_or_create_inbox
return redirect_to app_onboarding_inbox_setup_url(account_id: account_id) if return_to == 'onboarding'
if already_exists
redirect_to app_instagram_inbox_settings_url(account_id: account_id, inbox_id: inbox.id)
else
@@ -149,6 +151,10 @@ class Instagram::CallbacksController < ApplicationController
verify_instagram_token(params[:state])
end
def return_to
instagram_token_return_to(params[:state])
end
def oauth_code
params[:code]
end
@@ -14,4 +14,11 @@ class Microsoft::CallbacksController < OauthCallbackController
def imap_address
'outlook.office365.com'
end
# Exchange Online's SMTP AUTH (XOAUTH2) rejects proxy addresses in the SASL `user=` field;
# it must match the token's UPN. `preferred_username` is the documented v2.0 claim;
# `upn` is the v1.0 fallback.
def imap_login_identity
users_data['preferred_username'] || users_data['upn'] || super
end
end
+25 -2
View File
@@ -16,6 +16,8 @@ class OauthCallbackController < ApplicationController
def handle_response
inbox, already_exists = find_or_create_inbox
return redirect_to app_onboarding_inbox_setup_url(account_id: account.id) if return_to == 'onboarding'
if already_exists
redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
else
@@ -44,7 +46,7 @@ class OauthCallbackController < ApplicationController
def update_channel(channel_email)
channel_email.update!({
imap_login: users_data['email'], imap_address: imap_address,
imap_login: imap_login_identity, imap_address: imap_address,
imap_port: '993', imap_enabled: true,
provider: provider_name,
provider_config: {
@@ -55,6 +57,13 @@ class OauthCallbackController < ApplicationController
})
end
# Identity used as the IMAP/SMTP login (SASL XOAUTH2 `user=` field). Defaults to the
# id_token's email claim; providers override when their server requires a different
# claim (e.g. Microsoft SMTP requires UPN).
def imap_login_identity
users_data['email']
end
def provider_name
raise NotImplementedError
end
@@ -81,10 +90,19 @@ class OauthCallbackController < ApplicationController
decoded_token[0]
end
# The sgid purpose carries the onboarding return hint (see
# OauthAuthorizationController#state). Try the onboarding purpose first — a match
# both resolves the account and records the return target — then fall back to the
# default purpose used by every other caller.
def account_from_signed_id
raise ActionController::BadRequest, 'Missing state variable' if params[:state].blank?
account = GlobalID::Locator.locate_signed(params[:state])
if (account = GlobalID::Locator.locate_signed(params[:state], for: 'onboarding'))
@return_to = 'onboarding'
else
account = GlobalID::Locator.locate_signed(params[:state])
end
raise 'Invalid or expired state' if account.nil?
account
@@ -94,6 +112,11 @@ class OauthCallbackController < ApplicationController
@account ||= account_from_signed_id
end
def return_to
account # resolving the sgid records which purpose matched
@return_to
end
# Fallback name, for when name field is missing from users_data
def fallback_name
users_data['email'].split('@').first.parameterize.titleize
@@ -0,0 +1,31 @@
class Public::Api::V1::Portals::SearchController < Public::Api::V1::Portals::BaseController
before_action :ensure_custom_domain_request, only: [:index]
before_action :portal
before_action :set_portal_layout
before_action :set_view_variant
before_action :ensure_portal_feature_enabled
layout 'portal'
def index
@query = params[:query].to_s.strip
@articles = @portal.articles.published.includes(:category).where(locale: params[:locale])
search_articles
@articles = @articles.page(params[:page]).per(10)
end
private
def search_articles
@articles = @query.present? ? @articles.search(search_params) : @articles.none
end
def search_params
params.permit(:query, :locale, :sort, :status, :page).tap do |permitted|
permitted[:query] = @query
end
end
end
Public::Api::V1::Portals::SearchController.prepend_mod_with('Public::Api::V1::Portals::SearchController')
@@ -20,6 +20,8 @@ class Tiktok::CallbacksController < ApplicationController
def process_successful_authorization
inbox, already_exists = find_or_create_inbox
return redirect_to app_onboarding_inbox_setup_url(account_id: account_id) if return_to == 'onboarding'
if already_exists
redirect_to app_tiktok_inbox_settings_url(account_id: account_id, inbox_id: inbox.id)
else
@@ -127,6 +129,10 @@ class Tiktok::CallbacksController < ApplicationController
@account_id ||= verify_tiktok_token(params[:state])
end
def return_to
tiktok_token_return_to(params[:state])
end
def account
@account ||= Account.find(account_id)
end