diff --git a/Dockerfile b/Dockerfile index 52531940..23ee1f4b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,6 +36,7 @@ COPY ./config/shakapacker.yml ./config/shakapacker.yml COPY ./postcss.config.js ./postcss.config.js COPY ./tailwind.config.js ./tailwind.config.js COPY ./tailwind.form.config.js ./tailwind.form.config.js +COPY ./tailwind.dynamic.config.js ./tailwind.dynamic.config.js COPY ./tailwind.application.config.js ./tailwind.application.config.js COPY ./app/javascript ./app/javascript COPY ./app/views ./app/views diff --git a/app/controllers/account_configs_controller.rb b/app/controllers/account_configs_controller.rb index cf128ef9..66d0967b 100644 --- a/app/controllers/account_configs_controller.rb +++ b/app/controllers/account_configs_controller.rb @@ -23,7 +23,8 @@ class AccountConfigsController < ApplicationController AccountConfig::WITH_SIGNATURE_ID, AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::REQUIRE_SIGNING_REASON_KEY, - AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY + AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY, + AccountConfig::ENABLE_MCP_KEY ].freeze InvalidKey = Class.new(StandardError) diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index c3f1dd42..df4bcee7 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -117,7 +117,7 @@ module Api conditions: [%i[field_uuid value action operation]], options: [%i[value uuid]], validation: %i[message pattern min max step], - areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] + areas: [%i[uuid x y w h cell_w attachment_uuid option_uuid page]] }]] } ] diff --git a/app/controllers/mcp_controller.rb b/app/controllers/mcp_controller.rb new file mode 100644 index 00000000..8d7ca288 --- /dev/null +++ b/app/controllers/mcp_controller.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class McpController < ActionController::API + before_action :authenticate_user! + before_action :verify_mcp_enabled! + + before_action do + authorize!(:manage, :mcp) + end + + def call + return head :ok if request.raw_post.blank? + + body = JSON.parse(request.raw_post) + + result = Mcp::HandleRequest.call(body, current_user, current_ability) + + if result + render json: result + else + head :accepted + end + rescue CanCan::AccessDenied + render json: { jsonrpc: '2.0', id: nil, error: { code: -32_603, message: 'Forbidden' } }, status: :forbidden + rescue JSON::ParserError + render json: { jsonrpc: '2.0', id: nil, error: { code: -32_700, message: 'Parse error' } }, status: :bad_request + end + + private + + def authenticate_user! + render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user + end + + def verify_mcp_enabled! + return if Docuseal.multitenant? + + return if AccountConfig.exists?(account_id: current_user.account_id, + key: AccountConfig::ENABLE_MCP_KEY, + value: true) + + render json: { error: 'MCP is disabled' }, status: :forbidden + end + + def current_user + @current_user ||= user_from_api_key + end + + def user_from_api_key + token = request.headers['Authorization'].to_s[/\ABearer\s+(.+)\z/, 1] + + return if token.blank? + + sha256 = Digest::SHA256.hexdigest(token) + + User.joins(:mcp_tokens).active.find_by(mcp_tokens: { sha256:, archived_at: nil }) + end +end diff --git a/app/controllers/mcp_settings_controller.rb b/app/controllers/mcp_settings_controller.rb new file mode 100644 index 00000000..78ee4681 --- /dev/null +++ b/app/controllers/mcp_settings_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class McpSettingsController < ApplicationController + load_and_authorize_resource :mcp_token, parent: false + + before_action do + authorize!(:manage, :mcp) + end + + def index + @mcp_tokens = @mcp_tokens.active.order(id: :desc) + end + + def create + @mcp_token = current_user.mcp_tokens.new(mcp_token_params) + + if @mcp_token.save + @mcp_tokens = [@mcp_token] + + render :index, status: :created + else + render turbo_stream: turbo_stream.replace(:modal, template: 'mcp_settings/new'), status: :unprocessable_content + end + end + + def destroy + @mcp_token.update!(archived_at: Time.current) + + redirect_back fallback_location: settings_mcp_index_path, notice: I18n.t('mcp_token_has_been_removed') + end + + private + + def mcp_token_params + params.require(:mcp_token).permit(:name) + end +end diff --git a/app/controllers/preview_document_page_controller.rb b/app/controllers/preview_document_page_controller.rb index 5e59e74a..d0b69f8d 100644 --- a/app/controllers/preview_document_page_controller.rb +++ b/app/controllers/preview_document_page_controller.rb @@ -6,9 +6,17 @@ class PreviewDocumentPageController < ActionController::API FORMAT = Templates::ProcessDocument::FORMAT def show - attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment) + result_data = + ApplicationRecord.signed_id_verifier.verified(params[:signed_key], purpose: :attachment) - attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid) if attachment_uuid + attachment = + if result_data.is_a?(Array) && result_data.compact_blank.size == 2 + attachment_id, attachment_uuid = result_data + + ActiveStorage::Attachment.find_by(id: attachment_id, uuid: attachment_uuid) + elsif result_data + ActiveStorage::Attachment.find_by(uuid: result_data) + end return head :not_found unless attachment diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index ad5d6455..ed9c2629 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -172,6 +172,8 @@ class StartFormController < ApplicationController submitters: [submitter], source: :link) + Submissions::CreateFromSubmitters.maybe_set_dynamic_documents(submitter.submission) + submitter.account_id = submitter.submission.account_id submitter diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index c39e99e5..d0caf466 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -51,18 +51,7 @@ class SubmissionsController < ApplicationController emails: params[:emails], params: params.merge('send_completed_email' => true)) else - submissions_attrs = submissions_params[:submission].to_h.values - - submissions_attrs, _, new_fields = - Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template, add_fields: true) - - Submissions.create_from_submitters(template: @template, - user: current_user, - source: :invite, - submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random', - submissions_attrs:, - new_fields:, - params: params.merge('send_completed_email' => true)) + create_submissions(@template, submissions_params, params) end WebhookUrls.enqueue_events(submissions, 'submission.created') @@ -97,6 +86,21 @@ class SubmissionsController < ApplicationController private + def create_submissions(template, submissions_params, params) + submissions_attrs = submissions_params[:submission].to_h.values + + submissions_attrs, _, new_fields = + Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, template, add_fields: true) + + Submissions.create_from_submitters(template: template, + user: current_user, + source: :invite, + submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random', + submissions_attrs:, + new_fields:, + params: params.merge('send_completed_email' => true)) + end + def save_template_message(template, params) template.preferences['request_email_subject'] = params[:subject] if params[:subject].present? template.preferences['request_email_body'] = params[:body] if params[:body].present? diff --git a/app/controllers/template_documents_controller.rb b/app/controllers/template_documents_controller.rb index 51fc4111..db8ba66c 100644 --- a/app/controllers/template_documents_controller.rb +++ b/app/controllers/template_documents_controller.rb @@ -16,7 +16,7 @@ class TemplateDocumentsController < ApplicationController old_fields_hash = @template.fields.hash - documents = Templates::CreateAttachments.call(@template, params, extract_fields: true) + documents, = Templates::CreateAttachments.call(@template, params, extract_fields: true) schema = documents.map do |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } @@ -27,7 +27,7 @@ class TemplateDocumentsController < ApplicationController fields: old_fields_hash == @template.fields.hash ? nil : @template.fields, submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters, documents: documents.as_json( - methods: %i[metadata signed_uuid], + methods: %i[metadata signed_key], include: { preview_images: { methods: %i[url metadata filename] } } diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index 0d6db6f7..b9496320 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -35,7 +35,7 @@ class TemplatesController < ApplicationController @template_data = @template.as_json.merge( documents: @template.schema_documents.as_json( - methods: %i[metadata signed_uuid], + methods: %i[metadata signed_key], include: { preview_images: { methods: %i[url metadata filename] } } ) ).to_json @@ -95,10 +95,11 @@ class TemplatesController < ApplicationController def template_params params.require(:template).permit( :name, - { schema: [[:attachment_uuid, :google_drive_file_id, :name, + { schema: [[:attachment_uuid, :google_drive_file_id, :name, :dynamic, { conditions: [%i[field_uuid value action operation]] }]], submitters: [%i[name uuid is_requester linked_to_uuid invite_via_field_uuid invite_by_uuid optional_invite_by_uuid email order]], + variables_schema: {}, fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, :title, :description, :prefillable, @@ -107,7 +108,7 @@ class TemplatesController < ApplicationController conditions: [%i[field_uuid value action operation]], options: [%i[value uuid]], validation: %i[message pattern min max step], - areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] } + areas: [%i[uuid x y w h cell_w attachment_uuid option_uuid page]] }]] } ) end end diff --git a/app/controllers/templates_debug_controller.rb b/app/controllers/templates_debug_controller.rb index 4ff31835..54fa23e1 100644 --- a/app/controllers/templates_debug_controller.rb +++ b/app/controllers/templates_debug_controller.rb @@ -9,20 +9,22 @@ class TemplatesDebugController < ApplicationController schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] } attachment = @template.documents.find { |a| schema_uuids[a.uuid] } - data = attachment.download + if attachment + data = attachment.download - unless attachment.image? - pdf = HexaPDF::Document.new(io: StringIO.new(data)) + unless attachment.image? + pdf = HexaPDF::Document.new(io: StringIO.new(data)) - fields = Templates::FindAcroFields.call(pdf, attachment, data) - end + fields = Templates::FindAcroFields.call(pdf, attachment, data) + end - fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank? + # fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank? - attachment.metadata['pdf'] ||= {} - attachment.metadata['pdf']['fields'] = fields + attachment.metadata['pdf'] ||= {} + attachment.metadata['pdf']['fields'] = fields - @template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment])) + @template.update!(fields: Templates::ProcessDocument.normalize_attachment_fields(@template, [attachment])) + end debug_file if DEBUG_FILE.present? @@ -34,7 +36,7 @@ class TemplatesDebugController < ApplicationController @template_data = @template.as_json.merge( documents: @template.schema_documents.as_json( - methods: %i[metadata signed_uuid], + methods: %i[metadata signed_key], include: { preview_images: { methods: %i[url metadata filename] } } ) ).to_json @@ -58,9 +60,16 @@ class TemplatesDebugController < ApplicationController params = { files: [file] } - documents = Templates::CreateAttachments.call(@template, params) + documents, dynamic_documents = Templates::CreateAttachments.call(@template, params, + dynamic: DEBUG_FILE.ends_with?('.docx')) - schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } } + schema = documents.map do |doc| + { + attachment_uuid: doc.uuid, + name: doc.filename.base, + dynamic: dynamic_documents.find { |e| e.uuid == doc.uuid }.present? + } + end @template.update!(schema:) end diff --git a/app/controllers/templates_preview_controller.rb b/app/controllers/templates_preview_controller.rb index a3f4ec37..b9325bfa 100644 --- a/app/controllers/templates_preview_controller.rb +++ b/app/controllers/templates_preview_controller.rb @@ -12,7 +12,7 @@ class TemplatesPreviewController < ApplicationController @template_data = @template.as_json.merge( documents: @template.schema_documents.as_json( - methods: %i[metadata signed_uuid], + methods: %i[metadata signed_key], include: { preview_images: { methods: %i[url metadata filename] } } ) ).to_json diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb index ddee4e90..7d8b1bae 100644 --- a/app/controllers/templates_uploads_controller.rb +++ b/app/controllers/templates_uploads_controller.rb @@ -12,7 +12,7 @@ class TemplatesUploadsController < ApplicationController save_template!(@template, url_params) - documents = Templates::CreateAttachments.call(@template, url_params || params, extract_fields: true) + documents, = Templates::CreateAttachments.call(@template, url_params || params, extract_fields: true) schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } } if @template.fields.blank? diff --git a/app/javascript/application.js b/app/javascript/application.js index fe53cc5b..920cba82 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -160,6 +160,7 @@ safeRegisterElement('template-builder', class extends HTMLElement { this.app = createApp(TemplateBuilder, { template, customFields: reactive(JSON.parse(this.dataset.customFields || '[]')), + dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')), backgroundColor: '#faf7f5', locale: this.dataset.locale, withPhone: this.dataset.withPhone === 'true', @@ -177,6 +178,7 @@ safeRegisterElement('template-builder', class extends HTMLElement { withSendButton: this.dataset.withSendButton !== 'false', withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false', withConditions: this.dataset.withConditions === 'true', + withDynamicDocuments: this.dataset.withDynamicDocuments === 'true', withGoogleDrive: this.dataset.withGoogleDrive === 'true', withReplaceAndCloneUpload: true, withDownload: true, diff --git a/app/javascript/template_builder/area.vue b/app/javascript/template_builder/area.vue index e071e9ee..bbf1a357 100644 --- a/app/javascript/template_builder/area.vue +++ b/app/javascript/template_builder/area.vue @@ -31,144 +31,26 @@ /> -