diff --git a/Gemfile b/Gemfile index 35855b67..9705eaa8 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,7 @@ gem 'faraday' gem 'google-cloud-storage', require: false gem 'hexapdf' gem 'image_processing' +gem 'jwt' gem 'lograge' gem 'mysql2', require: false gem 'oj' diff --git a/Gemfile.lock b/Gemfile.lock index 509eedc7..f892df69 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -540,6 +540,7 @@ DEPENDENCIES google-cloud-storage hexapdf image_processing + jwt letter_opener_web lograge mysql2 diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb new file mode 100644 index 00000000..a63f0ce7 --- /dev/null +++ b/app/controllers/api/submissions_controller.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Api + class SubmissionsController < ApiBaseController + def create + template = current_account.templates.find(params[:template_id]) + + submissions = + if params[:emails].present? + Submissions.create_from_emails(template:, + user: current_user, + send_email: params[:send_email] != 'false', + emails: params[:emails]) + else + Submissions.create_from_submitters(template:, + user: current_user, + send_email: params[:send_email] != 'false', + submissions_attrs: submissions_params[:submission]) + end + + submitters = submissions.flat_map(&:submitters) + + if params[:send_email] != 'false' + submitters.each do |submitter| + SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later! + end + end + + render json: submitters + end + + private + + def submissions_params + params.permit(submission: [{ submitters: [%i[uuid name email]] }]) + end + end +end diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index 6675a684..af7ae691 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -2,6 +2,10 @@ module Api class TemplatesController < ApiBaseController + def index + render json: current_account.templates + end + def update @template = current_account.templates.find(params[:id]) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 7e5cfc78..f9fe3b79 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -17,9 +17,15 @@ class SubmissionsController < ApplicationController def create submissions = if params[:emails].present? - create_submissions_from_emails + Submissions.create_from_emails(template: @template, + user: current_user, + send_email: params[:send_email] == '1', + emails: params[:emails]) else - create_submissions_from_submitters + Submissions.create_from_submitters(template: @template, + user: current_user, + send_email: params[:send_email] == '1', + submissions_attrs: submissions_params[:submission].to_h.values) end submitters = submissions.flat_map(&:submitters) @@ -45,30 +51,6 @@ class SubmissionsController < ApplicationController private - def create_submissions_from_emails - emails = params[:emails].to_s.scan(User::EMAIL_REGEXP) - - emails.map do |email| - submission = @template.submissions.new(created_by_user: current_user) - submission.submitters.new(email:, uuid: @template.submitters.first['uuid'], - sent_at: params[:send_email] == '1' ? Time.current : nil) - - submission.tap(&:save!) - end - end - - def create_submissions_from_submitters - submissions_params[:submission].to_h.map do |_, attrs| - submission = @template.submissions.new(created_by_user: current_user) - - attrs[:submitters].each do |submitter_attrs| - submission.submitters.new(**submitter_attrs, sent_at: params[:send_email] == '1' ? Time.current : nil) - end - - submission.tap(&:save!) - end - end - def submissions_params params.permit(submission: { submitters: [%i[uuid email]] }) end diff --git a/app/mailers/submitter_mailer.rb b/app/mailers/submitter_mailer.rb index c5774baa..1c90288f 100644 --- a/app/mailers/submitter_mailer.rb +++ b/app/mailers/submitter_mailer.rb @@ -3,9 +3,9 @@ class SubmitterMailer < ApplicationMailer DEFAULT_MESSAGE = %(You have been invited to submit the "%s" form:) - def invitation_email(submitter, message: format(DEFAULT_MESSAGE, name: submitter.submission.template.name)) + def invitation_email(submitter, message: '') @submitter = submitter - @message = message + @message = message.presence || format(DEFAULT_MESSAGE, name: submitter.submission.template.name) mail(to: @submitter.email, subject: 'You have been invited to submit a form', diff --git a/app/models/user.rb b/app/models/user.rb index 959ba4dd..7e7e023b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,6 +22,7 @@ # role :string not null # sign_in_count :integer default(0), not null # unlock_token :string +# uuid :text not null # created_at :datetime not null # updated_at :datetime not null # account_id :bigint not null @@ -32,6 +33,7 @@ # index_users_on_email (email) UNIQUE # index_users_on_reset_password_token (reset_password_token) UNIQUE # index_users_on_unlock_token (unlock_token) UNIQUE +# index_users_on_uuid (uuid) UNIQUE # # Foreign Keys # @@ -49,6 +51,7 @@ class User < ApplicationRecord devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant? attribute :role, :string, default: 'admin' + attribute :uuid, :string, default: -> { SecureRandom.uuid } scope :active, -> { where(deleted_at: nil) } diff --git a/config/application.rb b/config/application.rb index 920df247..7991f878 100644 --- a/config/application.rb +++ b/config/application.rb @@ -9,6 +9,8 @@ require 'action_view/railtie' require 'action_mailer/railtie' require 'active_job/railtie' +require './lib/api_path_consider_json_middleware' + Bundler.require(*Rails.groups) module DocuSeal @@ -26,5 +28,6 @@ module DocuSeal config.action_view.frozen_string_literal = true config.middleware.insert_before ActionDispatch::Static, Rack::Deflater + config.middleware.insert_before ActionDispatch::Static, ApiPathConsiderJsonMiddleware end end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 6bf18a0a..8cb07803 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +require './lib/auth_with_token_strategy' + +Warden::Strategies.add(:auth_token, AuthWithTokenStrategy) + # Assuming you have not yet modified this file, each configuration option below # is set to its default value. Note that some are commented out while others # are not: uncommented lines are intended to protect your configuration from @@ -272,10 +276,10 @@ Devise.setup do |config| # If you want to use other strategies, that are not supported by Devise, or # change the failure app, you can configure them inside the config.warden block. # - # config.warden do |manager| - # manager.intercept_401 = false - # manager.default_strategies(scope: :user).unshift :some_external_strategy - # end + config.warden do |manager| + # manager.intercept_401 = false + manager.default_strategies(scope: :user).unshift(:auth_token) + end # ==> Mountable engine configurations # When using Devise inside an engine, let's call it `MyEngine`, and this engine diff --git a/config/routes.rb b/config/routes.rb index 03a89a6e..f0e78d52 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,9 +27,11 @@ Rails.application.routes.draw do end end - namespace :api do + namespace :api, defaults: { format: :json } do resources :attachments, only: %i[create] - resources :templates, only: %i[update] do + resources :submissions, only: %i[create] + resources :templates, only: %i[update index] do + resources :submissions, only: %i[create] resources :documents, only: %i[create destroy], controller: 'templates_documents' end end diff --git a/db/migrate/20230803200825_add_uuid_to_users.rb b/db/migrate/20230803200825_add_uuid_to_users.rb new file mode 100644 index 00000000..f29bd86c --- /dev/null +++ b/db/migrate/20230803200825_add_uuid_to_users.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AddUuidToUsers < ActiveRecord::Migration[7.0] + class MigrationUser < ApplicationRecord + self.table_name = 'users' + end + + def up + add_column :users, :uuid, :text + add_index :users, :uuid, unique: true + + MigrationUser.all.each do |user| + user.update_columns(uuid: SecureRandom.uuid) + end + + change_column_null :users, :uuid, false + end + + def down + drop_column :users, :uuid + end +end diff --git a/db/schema.rb b/db/schema.rb index a053d26d..3760170f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_07_26_062428) do +ActiveRecord::Schema[7.0].define(version: 2023_08_03_200825) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -139,10 +139,12 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_26_062428) do t.datetime "deleted_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.text "uuid", null: false t.index ["account_id"], name: "index_users_on_account_id" t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true + t.index ["uuid"], name: "index_users_on_uuid", unique: true end add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" diff --git a/lib/api_path_consider_json_middleware.rb b/lib/api_path_consider_json_middleware.rb new file mode 100644 index 00000000..e93ee86c --- /dev/null +++ b/lib/api_path_consider_json_middleware.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class ApiPathConsiderJsonMiddleware + def initialize(app) + @app = app + end + + def call(env) + if env['PATH_INFO'].starts_with?('/api') && + !env['PATH_INFO'].ends_with?('/documents') && + !env['PATH_INFO'].ends_with?('/attachments') + env['CONTENT_TYPE'] = 'application/json' + end + + @app.call(env) + end +end diff --git a/lib/auth_with_token_strategy.rb b/lib/auth_with_token_strategy.rb new file mode 100644 index 00000000..4b7f6b0b --- /dev/null +++ b/lib/auth_with_token_strategy.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AuthWithTokenStrategy < Devise::Strategies::Base + def valid? + request.headers['X-Auth-Token'].present? + end + + def authenticate! + payload = JsonWebToken.decode(request.headers['X-Auth-Token']) + + user = User.find_by(uuid: payload['uuid']) + + if user + success!(user) + else + fail!('Invalid token') + end + rescue JWT::VerificationError + fail!('Invalid token') + end +end diff --git a/lib/json_web_token.rb b/lib/json_web_token.rb new file mode 100644 index 00000000..a19dc8d7 --- /dev/null +++ b/lib/json_web_token.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module JsonWebToken + module_function + + def encode(payload) + JWT.encode(payload, Rails.application.secrets.secret_key_base) + end + + def decode(token) + JWT.decode(token, Rails.application.secrets.secret_key_base)[0] + end +end diff --git a/lib/submissions.rb b/lib/submissions.rb index 040aa355..bdefe66d 100644 --- a/lib/submissions.rb +++ b/lib/submissions.rb @@ -10,4 +10,33 @@ module Submissions submission.save! end + + def create_from_emails(template:, user:, emails:, send_email: false) + emails = emails.to_s.scan(User::EMAIL_REGEXP) + + emails.map do |email| + submission = template.submissions.new(created_by_user: user) + submission.submitters.new(email:, uuid: template.submitters.first['uuid'], + sent_at: send_email ? Time.current : nil) + + submission.tap(&:save!) + end + end + + def create_from_submitters(template:, user:, submissions_attrs:, send_email: false) + submissions_attrs.map do |attrs| + submission = template.submissions.new(created_by_user: user) + + attrs[:submitters].each_with_index do |submitter_attrs, index| + uuid = + submitter_attrs[:uuid].presence || + template.submitters.find { |e| e['name'] == submitter_attrs[:name] }&.dig('uuid') || + template.submitters[index]&.dig('uuid') + + submission.submitters.new(**submitter_attrs, uuid:, sent_at: send_email ? Time.current : nil) + end + + submission.tap(&:save!) + end + end end