diff --git a/Gemfile b/Gemfile index 5edacf85..0473c2c0 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,8 @@ gem 'image_processing' gem 'lograge' gem 'mysql2', require: false gem 'oj' +gem 'omniauth-google-oauth2' +gem 'omniauth-rails_csrf_protection' gem 'pagy' gem 'pg', require: false gem 'premailer-rails' diff --git a/Gemfile.lock b/Gemfile.lock index a0f7cb73..4988cb69 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -232,6 +232,7 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) hashdiff (1.0.1) + hashie (5.0.0) hexapdf (0.32.2) cmdparse (~> 3.0, >= 3.0.3) geom2d (~> 0.3) @@ -282,6 +283,7 @@ GEM minitest (5.18.1) msgpack (1.7.1) multi_json (1.15.0) + multi_xml (0.6.0) multipart-post (2.3.0) mysql2 (0.5.5) net-http-persistent (4.0.2) @@ -298,7 +300,29 @@ GEM nio4r (2.5.9) nokogiri (1.15.2-arm64-darwin) racc (~> 1.4) + oauth2 (2.0.9) + faraday (>= 0.17.3, < 3.0) + jwt (>= 1.0, < 3.0) + multi_xml (~> 0.5) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0) + version_gem (~> 1.1) oj (3.15.0) + omniauth (2.1.1) + hashie (>= 3.4.6) + rack (>= 2.2.3) + rack-protection + omniauth-google-oauth2 (1.1.1) + jwt (>= 2.0) + oauth2 (~> 2.0.6) + omniauth (~> 2.0) + omniauth-oauth2 (~> 1.8.0) + omniauth-oauth2 (1.8.0) + oauth2 (>= 1.4, < 3) + omniauth (~> 2.0) + omniauth-rails_csrf_protection (1.0.1) + actionpack (>= 4.2) + omniauth (~> 2.0) openssl (3.1.0) orm_adapter (0.5.0) os (1.1.4) @@ -326,6 +350,8 @@ GEM nio4r (~> 2.0) racc (1.7.1) rack (2.2.7) + rack-protection (3.0.6) + rack rack-proxy (0.7.6) rack rack-test (2.1.0) @@ -444,6 +470,9 @@ GEM simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) smart_properties (1.17.0) + snaky_hash (2.0.1) + hashie + version_gem (~> 1.1, >= 1.1.1) sqlite3 (1.5.4) mini_portile2 (~> 2.8.0) strip_attributes (1.13.0) @@ -462,6 +491,7 @@ GEM uber (0.1.0) unicode-display_width (2.4.2) uniform_notifier (1.16.0) + version_gem (1.1.3) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.0) @@ -507,6 +537,8 @@ DEPENDENCIES lograge mysql2 oj + omniauth-google-oauth2 + omniauth-rails_csrf_protection pagy pg premailer-rails diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 60d8acd8..5c568870 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -40,6 +40,8 @@ class AccountsController < ApplicationController end def app_url_params + return {} if params[:encrypted_config].blank? + params.require(:encrypted_config).permit(:value) end end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 95afc887..4818da9b 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -4,6 +4,7 @@ class DashboardController < ApplicationController skip_before_action :authenticate_user!, only: %i[index] def index + return redirect_to Docuseal::PRODUCT_URL, allow_other_host: true if Docuseal.multitenant? && !signed_in? return render 'pages/landing' unless signed_in? templates = current_account.templates.active.preload(:author).order(id: :desc) diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb new file mode 100644 index 00000000..de6d1910 --- /dev/null +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class OmniauthCallbacksController < Devise::OmniauthCallbacksController + def google_oauth2 + @user = Users.from_omniauth(request.env['omniauth.auth']) + + if @user.persisted? + flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: 'Google') + + sign_in_and_redirect @user, event: :authentication + else + redirect_to new_registration_path(oauth_callback: true, user: @user.slice(:email, :first_name, :last_name)), + notice: 'Please complete registration with Google auth' + end + end +end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index f0f0028c..68cc3a12 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,6 +1,16 @@ # frozen_string_literal: true class RegistrationsController < Devise::RegistrationsController + prepend_before_action :require_no_authentication, only: [:show] + + def show; end + + def create + super + + Accounts.create_default_template(resource.account) if resource.account.persisted? + end + private def build_resource(_hash = {}) @@ -8,17 +18,21 @@ class RegistrationsController < Devise::RegistrationsController account.timezone = Accounts.normalize_timezone(account.timezone) self.resource = account.users.new(user_params) + + account.name ||= "#{resource.full_name}'s Company" if params[:action] == 'create' end def user_params return {} if params[:user].blank? - params.require(:user).permit(:first_name, :last_name, :email, :password) + params.require(:user).permit(:first_name, :last_name, :email, :password).compact_blank.tap do |attrs| + attrs[:password] ||= SecureRandom.hex if params[:action] == 'create' + end end def account_params return {} if params[:account].blank? - params.require(:account).permit(:name, :timezone) + params.require(:account).permit(:name, :timezone).compact_blank end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 00000000..f899f160 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class SessionsController < Devise::SessionsController + def create + if Docuseal.multitenant? && !User.exists?(email: sign_in_params[:email]) + return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)), + notice: 'Create a new account' + end + + super + end +end diff --git a/app/models/submitter.rb b/app/models/submitter.rb index 29d55710..35c1a3e6 100644 --- a/app/models/submitter.rb +++ b/app/models/submitter.rb @@ -32,7 +32,7 @@ class Submitter < ApplicationRecord belongs_to :submission attribute :values, :string, default: -> { {} } - attribute :slug, :string, default: -> { SecureRandom.base58(10) } + attribute :slug, :string, default: -> { SecureRandom.base58(14) } serialize :values, JSON diff --git a/app/models/template.rb b/app/models/template.rb index e211488e..97db25d3 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -36,7 +36,7 @@ class Template < ApplicationRecord attribute :fields, :string, default: -> { [] } attribute :schema, :string, default: -> { [] } attribute :submitters, :string, default: -> { [{ name: DEFAULT_SUBMITTER_NAME, uuid: SecureRandom.uuid }] } - attribute :slug, :string, default: -> { SecureRandom.base58(10) } + attribute :slug, :string, default: -> { SecureRandom.base58(14) } serialize :fields, JSON serialize :schema, JSON diff --git a/app/models/user.rb b/app/models/user.rb index 8f4ce25e..20697d0c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -46,7 +46,7 @@ class User < ApplicationRecord belongs_to :account devise :database_authenticatable, :recoverable, :rememberable, :validatable, :trackable - devise :registerable if Docuseal.multitenant? + devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant? attribute :role, :string, default: 'admin' diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index 3d722106..881b82c8 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -1,6 +1,6 @@

Sign up

- <%= form_for('', as: resource_name, html: { class: 'space-y-6' }, url: registration_path) do |f| %> + <%= form_for('', as: resource_name, html: { class: 'space-y-6' }, url: new_registration_path) do |f| %>
<%= render 'devise/shared/error_messages', resource: %> <%= f.fields_for resource do |ff| %> @@ -16,7 +16,7 @@
<% end %> <%= f.fields_for resource do |ff| %> -
+
<%= ff.label :email, class: 'label' %> <%= ff.email_field :email, required: true, class: 'base-input' %>
@@ -25,14 +25,14 @@ <%= ff.hidden_field :timezone %>
- <%= ff.label :name, 'Company name', class: 'label' %> - <%= ff.text_field :name, required: true, class: 'base-input' %> + <%= ff.label :name, 'Company name (optional)', class: 'label' %> + <%= ff.text_field :name, class: 'base-input' %>
<% end %> <%= f.fields_for resource do |ff| %> -
+
<%= ff.label :password, class: 'label' %> - <%= ff.password_field :password, required: true, class: 'base-input' %> + <%= ff.password_field :password, required: !params[:oauth_callback], class: 'base-input' %>
<% end %>
diff --git a/app/views/devise/registrations/show.html.erb b/app/views/devise/registrations/show.html.erb new file mode 100644 index 00000000..1f69b61f --- /dev/null +++ b/app/views/devise/registrations/show.html.erb @@ -0,0 +1,18 @@ +
+

Sign up

+ <%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, method: :get) do |f| %> +
+
+ <%= f.label :email, class: 'label' %> + <%= f.email_field :email, autofocus: true, autocomplete: 'email', class: 'base-input' %> +
+
+
+ <%= f.button button_title(title: 'Sign up', disabled_with: 'Sign up'), name: 'sign_up', value: true, class: 'base-button' %> +
+ <% end %> + <% if devise_mapping.omniauthable? %> + <%= button_to button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %> + <% end %> + <%= render 'devise/shared/links' %> +
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 72dac935..cc8ebf7a 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -23,5 +23,8 @@ <%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
<% end %> + <% if devise_mapping.omniauthable? %> + <%= button_to button_title(title: 'Log in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %> + <% end %> <%= render 'devise/shared/links' %>
diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb index cc73180b..580391b5 100644 --- a/app/views/devise/shared/_links.html.erb +++ b/app/views/devise/shared/_links.html.erb @@ -3,7 +3,7 @@ <%= link_to 'Log in', new_session_path(resource_name), class: 'link link-hover' %> <% end %> <%- if devise_mapping.registerable? && controller_name != 'registrations' %> - <%= link_to 'Sign up', new_registration_path, class: 'link link-hover' %> + <%= link_to 'Sign up', registration_path, class: 'link link-hover' %> <% end %> <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> <%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %> @@ -14,9 +14,4 @@ <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'link link-hover' %> <% end %> - <%- if devise_mapping.omniauthable? %> - <%- resource_class.omniauth_providers.each do |provider| %> - <%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), class: 'link link-hover', data: { turbo: false } %> - <% end %> - <% end %> diff --git a/app/views/icons/_brand_google.html.erb b/app/views/icons/_brand_google.html.erb new file mode 100644 index 00000000..8b8e743e --- /dev/null +++ b/app/views/icons/_brand_google.html.erb @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index 709b7d8c..7c33ee23 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -29,8 +29,8 @@ <% end %>
  • - <%= link_to destroy_user_session_path, data: { turbo_method: :delete } do %> - <%= svg_icon('logout', class: 'w-5 h-5 stroke-2') %> + <%= button_to destroy_user_session_path, method: :delete, data: { turbo: false } do %> + <%= svg_icon('logout', class: 'w-5 h-5 stroke-2 inline') %> Sign out <% end %>
  • diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 11b76e9e..bc2d40bf 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -266,7 +266,7 @@ Devise.setup do |config| # ==> OmniAuth # Add a new OmniAuth provider. Check the wiki for more information on setting # up on your models and hooks. - # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + config.omniauth :google_oauth2, ENV.fetch('GOOGLE_CLIENT_ID', nil), ENV.fetch('GOOGLE_CLIENT_SECRET', nil), {} # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or diff --git a/config/routes.rb b/config/routes.rb index 7a902307..f20fa0cf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,11 +5,18 @@ Rails.application.routes.draw do root 'dashboard#index' - devise_for :users, path: '/', only: %i[sessions passwords] + devise_for :users, + path: '/', only: %i[sessions passwords omniauth_callbacks], + controllers: begin + options = { sessions: 'sessions' } + options[:omniauth_callbacks] = 'omniauth_callbacks' if Docuseal.multitenant? + options + end devise_scope :user do if Docuseal.multitenant? - resource :registration, only: %i[create], path: 'sign_up' do + resource :registration, only: %i[show], path: 'sign_up' + resource :registration, only: %i[create], path: 'new' do get '' => :new, as: :new end end diff --git a/lib/accounts.rb b/lib/accounts.rb index 6ac2973c..e814f0a7 100644 --- a/lib/accounts.rb +++ b/lib/accounts.rb @@ -16,7 +16,7 @@ module Accounts new_template = template.dup new_template.account = new_account - new_template.slug = SecureRandom.base58(10) + new_template.slug = SecureRandom.base58(14) new_template.save! @@ -28,6 +28,20 @@ module Accounts new_account end + def create_default_template(account) + template = Template.find(1) + + new_template = Template.find(1).dup + new_template.account_id = account.id + new_template.slug = SecureRandom.base58(14) + + new_template.save! + + Templates::CloneAttachments.call(template: new_template, original_template: template) + + new_template + end + def load_signing_certs(account) certs = if Docuseal.multitenant? diff --git a/lib/users.rb b/lib/users.rb new file mode 100644 index 00000000..775e3e97 --- /dev/null +++ b/lib/users.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Users + module_function + + def from_omniauth(oauth) + user = User.find_by(email: oauth.info.email) + + return user if user + + User.new(email: oauth.info.email, + first_name: oauth.extra.id_info.given_name, + last_name: oauth.extra.id_info.family_name) + end +end