diff --git a/Gemfile b/Gemfile index 5de7f90..47338e4 100644 --- a/Gemfile +++ b/Gemfile @@ -28,6 +28,8 @@ gem 'aws-sdk-s3', require: false gem 'google-api-client' gem 'rack-cors' gem 'sentry-raven' +gem 'sequenced' +gem 'sorcery' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console diff --git a/Gemfile.lock b/Gemfile.lock index eabe759..90a1745 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,6 +74,7 @@ GEM aws-sigv4 (~> 1.1) aws-sigv4 (1.1.0) aws-eventstream (~> 1.0, >= 1.0.2) + bcrypt (3.1.13) bootsnap (1.4.6) msgpack (~> 1.0) builder (3.2.4) @@ -133,10 +134,18 @@ GEM minitest (5.14.0) msgpack (1.3.3) multi_json (1.14.1) + multi_xml (0.6.0) multipart-post (2.1.1) nio4r (2.5.2) nokogiri (1.10.9) mini_portile2 (~> 2.4.0) + oauth (0.5.4) + oauth2 (1.4.4) + faraday (>= 0.8, < 2.0) + jwt (>= 1.0, < 3.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) os (1.1.0) pg (1.2.3) public_suffix (4.0.4) @@ -197,11 +206,18 @@ GEM semantic_range (2.3.0) sentry-raven (3.0.0) faraday (>= 1.0) + sequenced (3.2.0) + activerecord (>= 3.0) + activesupport (>= 3.0) signet (0.14.0) addressable (~> 2.3) faraday (>= 0.17.3, < 2.0) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) + sorcery (0.14.0) + bcrypt (~> 3.1) + oauth (~> 0.4, >= 0.4.4) + oauth2 (~> 1.0, >= 0.8.0) spring (2.1.0) spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) @@ -250,6 +266,8 @@ DEPENDENCIES rails sass-rails sentry-raven + sequenced + sorcery spring spring-watcher-listen turbolinks diff --git a/Procfile b/Procfile index c2c566e..05946fc 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,2 @@ +release: bundle exec rails db:migrate web: bundle exec puma -C config/puma.rb diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index 5918193..1f88c80 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,2 +1,2 @@ //= link_tree ../images -//= link_directory ../stylesheets .css +//= link application.css diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index e292b0e..71872c4 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -1,18 +1,3 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's - * vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS - * files in this directory. Styles in this file should be added after the last require_* statement. - * It is generally better to create a new file per style scope. - * - *= require_tree . - *= require_self - */ @import "bulma/sass/utilities/initial-variables"; @import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300;0,400;0,500;1,300;1,400&family=Lobster&family=Comfortaa:wght@400;500;600;700&display=swap'); @import url('https://use.fontawesome.com/releases/v5.12.0/css/all.css'); @@ -40,6 +25,8 @@ $light: #fff; @import 'bulma/bulma'; +@import './demo'; + .is-font-logo { font-family: 'Lobster', cursive; } @@ -60,4 +47,9 @@ $light: #fff; body { min-height: 100vh; +} + +.button.google { + background: rgb(225, 98, 89); + color: white; } \ No newline at end of file diff --git a/app/assets/stylesheets/demo.css.scss b/app/assets/stylesheets/demo.css.scss new file mode 100644 index 0000000..af052b0 --- /dev/null +++ b/app/assets/stylesheets/demo.css.scss @@ -0,0 +1,16 @@ +section.demo { + height: 90vh; + min-height: 600px; +} +.demo-form { + border: 1px solid $grey-light; + padding: 1em; + height: 100%; +} +section.demo .container, section.demo .columns, section.demo .column { + height: 100%; +} +#demo-success { + text-align: center; + padding-top: 1em; +} diff --git a/app/controllers/file_uploads_controller.rb b/app/controllers/file_uploads_controller.rb index 8ad47cc..ad487ca 100644 --- a/app/controllers/file_uploads_controller.rb +++ b/app/controllers/file_uploads_controller.rb @@ -2,7 +2,7 @@ class FileUploadsController < ApplicationController def show @form = Form.find_by!(token: params[:form_id]) @submission = @form.submissions.find(params[:submission_id]) - @file_upload = @submission.files_attachments.find(params[:id]) + @file_upload = @submission.files_attachments.find_by!(token: params[:id]) redirect_to url_for(@file_upload) end end diff --git a/app/controllers/oauths_controller.rb b/app/controllers/oauths_controller.rb new file mode 100644 index 0000000..a7e7f23 --- /dev/null +++ b/app/controllers/oauths_controller.rb @@ -0,0 +1,34 @@ +class OauthsController < ApplicationController + + # Sends the user on a trip to the provider, + # and after authorizing there back to the callback url. + def oauth + login_at(params[:provider]) + end + + def callback + provider = params[:provider] + if @user = login_from(provider) + redirect_to root_path, :notice => "Logged in from #{provider.titleize}!" + else + begin + @user = create_from(provider) + if authentication = @user.authentications.find_by(provider: provider) + authentication.update({ + access_token: @access_token.token, + refresh_token: @access_token.refresh_token, + expires_at: Time.at(@access_token.expires_at) + }) + end + + reset_session + auto_login(@user) + redirect_to root_path, :notice => "Logged in from #{provider.titleize}!" + rescue + Rails.logger.error("Failed to login from #{provider}") + redirect_to root_path, :alert => "Failed to login from #{provider.titleize}!" + end + end + end + +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 2b56102..9265f5a 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,29 +1,7 @@ -require 'google/apis/oauth2_v2' class SessionsController < ApplicationController def new reset_session - redirect_to auth_client.authorization_uri.to_s - end - - def auth - reset_session - if params[:error] - flash[:error] = 'Login failed' - redirect_to root_url - else - auth_client.code = params[:code] - auth_client.fetch_access_token! - - @user, @authentication = User.find_by_oauth_info(auth_client) - if @user.persisted? && @authentication.persisted? - session[:user_id] = @user.id.to_s - redirect_to forms_url - else - flash[:error] = 'Login failed' - redirect_to root_url - end - end end def destroy @@ -31,18 +9,4 @@ class SessionsController < ApplicationController redirect_to root_url end - private - - def auth_client - @auth_client ||= CLIENT_SECRETS.to_authorization.tap do |c| - c.update!( - scope: 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/spreadsheets', - redirect_uri: auth_url, - additional_parameters: { - 'access_type' => 'offline', # offline access - 'include_granted_scopes' => 'true' # incremental auth - } - ) - end - end end diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 518a3eb..9d801af 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -14,7 +14,7 @@ class SubmissionsController < ApplicationController respond_to do |format| if @submission.save format.html { redirect_to(@form.thank_you_url) if @form.thank_you_url.present? } - format.json { render(json: { success: true, data: @submission.data }) } + format.json { render(json: { success: true, submission: @submission.data }) } else format.html { redirect_to(@form.thank_you_url) if @form.thank_you_url.present? } format.json { render(json: { error: @submission.errors }, status: 422) } diff --git a/app/javascript/demo.js b/app/javascript/demo.js new file mode 100644 index 0000000..09fee25 --- /dev/null +++ b/app/javascript/demo.js @@ -0,0 +1,17 @@ +document.addEventListener('DOMContentLoaded', function() { + var demoForm = document.getElementById('demo-form'); + if (!demoForm) { + return; + } + + demoForm.addEventListener('tinyforms:submitted', function(e) { + console.log(e); + console.log(e.detail); + var name = document.getElementById('demo-submission-name'); + var demoFields = document.getElementById('demo-fields'); + var demoSucess = document.getElementById('demo-success'); + demoFields.style.display = 'none'; + demoSucess.style.display = 'block'; + name.innerText = e.detail.submission.Name; + }); +}); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 80561d9..fb7f3ef 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -9,6 +9,8 @@ require("@rails/activestorage").start() require("channels") require('burger_menu'); +require('tinyforms'); +require('demo'); // Uncomment to copy all static images under ../images to the output folder and reference // them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>) diff --git a/app/javascript/tinyforms.js b/app/javascript/tinyforms.js new file mode 100644 index 0000000..c1e242c --- /dev/null +++ b/app/javascript/tinyforms.js @@ -0,0 +1,51 @@ +window.tinyforms = { + init: function() { + var forms = document.querySelectorAll('form[data-tinyforms]'); + forms.forEach(function(form) { + form.addEventListener('submit', tinyforms.onSubmit); + }); + }, + + submitForm: function(form) { + return new Promise(function(resolve, reject) { + var action = form.getAttribute('action'); + var formData = new FormData(form); + + var xhr = new XMLHttpRequest(); + xhr.open('POST', action, true); + xhr.setRequestHeader("Accept", "application/json"); + xhr.onload = function () { + var response; + try { + response = JSON.parse(xhr.responseText); + } catch (e) { + response = xhr.responseText; + } + if (xhr.status >= 200 && xhr.status < 300) { + resolve(response); + } else { + reject(response); + } + }; + xhr.onerror = function() { + reject(xhr.responseText) + }; + xhr.send(formData); + }); + }, + + onSubmit: function(e) { + e.preventDefault(); + return tinyforms.submitForm(this) + .then((response) => { + this.dispatchEvent(new CustomEvent('tinyforms:submitted', {detail: { submission: response.submission, response: response }})); + }) + .catch((response) => { + this.dispatchEvent(new CustomEvent('tinyforms:error', {detail: { submission: response.submission, response: response }})); + }); + } +}; + +document.addEventListener('DOMContentLoaded', function() { + tinyforms.init(); +}); diff --git a/app/models/authentication.rb b/app/models/authentication.rb index 58fe264..6da1943 100644 --- a/app/models/authentication.rb +++ b/app/models/authentication.rb @@ -1,6 +1,8 @@ class Authentication < ApplicationRecord belongs_to :user + scope :for, -> (provider) { where(provider: provider) } + encrypts :access_token encrypts :refresh_token @@ -9,6 +11,7 @@ class Authentication < ApplicationRecord end def google_authorization + return nil unless provider == 'google' @google_authorization ||= CLIENT_SECRETS.to_authorization.tap do |c| c.access_token = self.access_token c.refresh_token = self.refresh_token diff --git a/app/models/form.rb b/app/models/form.rb index a1a13ca..b4fa31b 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -13,11 +13,11 @@ class Form < ApplicationRecord # TODO: use counter_cache option on association def submissions_count - submissions.count + @submissions_count ||= submissions.count end def last_submission_date - submissions.order(created_at: :desc).first&.created_at + @last_submission_date ||= submissions.order(created_at: :desc).first&.created_at end def deactivate!(reason = nil) diff --git a/app/models/submission.rb b/app/models/submission.rb index 899469c..4dfc874 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -2,6 +2,8 @@ class Submission < ApplicationRecord belongs_to :form has_many_attached :files + acts_as_sequenced scope: :form_id + validates_presence_of :data, if: :appended_at? def process_data(submitted_data) @@ -21,6 +23,10 @@ class Submission < ApplicationRecord JSON.dump(value) when 'tinyforms_now' Time.now.utc.to_formatted_s(:rfc822) + when 'tinyforms_token' + form.token + when 'tinyforms_id' + sequential_id when ActionDispatch::Http::UploadedFile # manually create the ActiveStorage attachment because we need the ID of the Attachment to create the URL # first the file needs to be uplaoded then we can create an Attachment @@ -31,7 +37,7 @@ class Submission < ApplicationRecord attachment = ActiveStorage::Attachment.new(record: self, name: 'files', blob: create_one.blob) attachment.save # return the URL that we use to show in the Spreadsheet - Rails.application.routes.url_helpers.file_upload_url(form_id: form, submission_id: self, id: attachment.id, host: DEFAULT_HOST) + Rails.application.routes.url_helpers.file_upload_url(form_id: form, submission_id: self, id: attachment.token, host: DEFAULT_HOST) else value.to_s end diff --git a/app/models/user.rb b/app/models/user.rb index 2918dc3..094cf4f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,40 +1,18 @@ class User < ApplicationRecord + authenticates_with_sorcery! has_many :authentications, dependent: :destroy has_many :forms, dependent: :destroy - def self.find_by_oauth_info(auth_client) - oauth = Google::Apis::Oauth2V2::Oauth2Service.new - oauth.authorization = auth_client - user_info = oauth.get_userinfo - - if user = User.find_by(google_id: user_info.id) - authentication = user.authentications.last - authentication.access_token = auth_client.access_token if auth_client.access_token.present? - authentication.refresh_token = auth_client.refresh_token if auth_client.refresh_token.present? - authentication.expires_at = Time.at(auth_client.expires_at) if auth_client.expires_at.present? - authentication.save - return user, authentication - else - user = User.create(name: user_info.name, email: user_info.email, google_id: user_info.id) - authentication = user.authentications.create( - access_token: auth_client.access_token, - refresh_token: auth_client.refresh_token, - expires_at: Time.at(auth_client.expires_at) - ) - return user, authentication - end - end - def deactivate!(reason = nil) # currently we only use deactivate if we get an authentication exception appending data to a spreadsheet authentications.last&.update(expires_at: Time.current) end def active? - authentications.last.present? && !authentications.last.expired? + authentications.any? { |a| !a.expired? } end def google_authorization - authentications.last.google_authorization + authentications.for(:google).last.google_authorization end end diff --git a/app/views/home/demo.html.erb b/app/views/home/demo.html.erb new file mode 100644 index 0000000..dfbdac7 --- /dev/null +++ b/app/views/home/demo.html.erb @@ -0,0 +1,85 @@ +
+
+
+
+
+

Demo

+

+ This short form is connected to the embedded <%= link_to 'document', DEMO_FORM.google_spreadsheet_url %> on the right.
+ Submit the form and see the update of the document in realtime. +

+ + + +
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+ +

Document not loading? <%= link_to 'Open it here', DEMO_FORM.google_spreadsheet_url %>.

+
+
+
+
+ diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb new file mode 100644 index 0000000..d08b511 --- /dev/null +++ b/app/views/layouts/_header.html.erb @@ -0,0 +1,29 @@ + diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index ff250d1..b001690 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,49 +11,12 @@ <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> - - -
+ - -
-
-
- -
-
- -
- <%= yield %> -
-
+ <%= render 'layouts/header' %> +
+ <%= yield %> +
diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb new file mode 100644 index 0000000..3f34469 --- /dev/null +++ b/app/views/sessions/new.html.erb @@ -0,0 +1,22 @@ +
+
+
+
+
+

+ Login +

+
+ + Login with Google +
+
+

+ As tinyforms builds on Google Sheets.
+ Simply login with your Google account. +

+
+
+
+
+
diff --git a/config/initializers/attachment_tokens.rb b/config/initializers/attachment_tokens.rb new file mode 100644 index 0000000..a73e466 --- /dev/null +++ b/config/initializers/attachment_tokens.rb @@ -0,0 +1,3 @@ +Rails.configuration.to_prepare do + ActiveStorage::Attachment.send(:has_secure_token) +end diff --git a/config/initializers/demo.rb b/config/initializers/demo.rb new file mode 100644 index 0000000..915b94c --- /dev/null +++ b/config/initializers/demo.rb @@ -0,0 +1 @@ +DEMO_FORM = Form.find_by(id: ENV['DEMO_FORM_ID']) diff --git a/config/initializers/sorcery.rb b/config/initializers/sorcery.rb new file mode 100644 index 0000000..4beedb8 --- /dev/null +++ b/config/initializers/sorcery.rb @@ -0,0 +1,532 @@ +# The first thing you need to configure is which modules you need in your app. +# The default is nothing which will include only core features (password encryption, login/logout). +# +# Available submodules are: :user_activation, :http_basic_auth, :remember_me, +# :reset_password, :session_timeout, :brute_force_protection, :activity_logging, +# :magic_login, :external +Rails.application.config.sorcery.submodules = [:reset_password, :external, :magic_login] + +# Here you can configure each submodule's features. +Rails.application.config.sorcery.configure do |config| + # -- core -- + # What controller action to call for non-authenticated users. You can also + # override the 'not_authenticated' method of course. + # Default: `:not_authenticated` + # + # config.not_authenticated_action = + + # When a non logged-in user tries to enter a page that requires login, save + # the URL he wants to reach, and send him there after login, using 'redirect_back_or_to'. + # Default: `true` + # + # config.save_return_to_url = + + # Set domain option for cookies; Useful for remember_me submodule. + # Default: `nil` + # + # config.cookie_domain = + + # Allow the remember_me cookie to be set through AJAX + # Default: `true` + # + # config.remember_me_httponly = + + # Set token randomness. (e.g. user activation tokens) + # The length of the result string is about 4/3 of `token_randomness`. + # Default: `15` + # + # config.token_randomness = + + # -- session timeout -- + # How long in seconds to keep the session alive. + # Default: `3600` + # + # config.session_timeout = + + # Use the last action as the beginning of session timeout. + # Default: `false` + # + # config.session_timeout_from_last_action = + + # Invalidate active sessions. Requires an `invalidate_sessions_before` timestamp column + # Default: `false` + # + # config.session_timeout_invalidate_active_sessions_enabled = + + # -- http_basic_auth -- + # What realm to display for which controller name. For example {"My App" => "Application"} + # Default: `{"application" => "Application"}` + # + # config.controller_to_realm_map = + + # -- activity logging -- + # Will register the time of last user login, every login. + # Default: `true` + # + # config.register_login_time = + + # Will register the time of last user logout, every logout. + # Default: `true` + # + # config.register_logout_time = + + # Will register the time of last user action, every action. + # Default: `true` + # + # config.register_last_activity_time = + + # -- external -- + # What providers are supported by this app + # i.e. [:twitter, :facebook, :github, :linkedin, :xing, :google, :liveid, :salesforce, :slack, :line]. + # Default: `[]` + # + config.external_providers = [:google] + + # You can change it by your local ca_file. i.e. '/etc/pki/tls/certs/ca-bundle.crt' + # Path to ca_file. By default use a internal ca-bundle.crt. + # Default: `'path/to/ca_file'` + # + # config.ca_file = + + # config.linkedin.key = "" + # config.linkedin.secret = "" + # config.linkedin.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=linkedin" + # config.linkedin.user_info_mapping = {first_name: "firstName", last_name: "lastName"} + # config.linkedin.scope = "r_basicprofile" + # + # + # For information about XING API: + # - user info fields go to https://dev.xing.com/docs/get/users/me + # + # config.xing.key = "" + # config.xing.secret = "" + # config.xing.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=xing" + # config.xing.user_info_mapping = {first_name: "first_name", last_name: "last_name"} + # + # + # Twitter will not accept any requests nor redirect uri containing localhost, + # Make sure you use 0.0.0.0:3000 to access your app in development + # + # config.twitter.key = "" + # config.twitter.secret = "" + # config.twitter.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=twitter" + # config.twitter.user_info_mapping = {:email => "screen_name"} + # + # config.facebook.key = "" + # config.facebook.secret = "" + # config.facebook.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=facebook" + # config.facebook.user_info_path = "me?fields=email" + # config.facebook.user_info_mapping = {:email => "email"} + # config.facebook.access_permissions = ["email"] + # config.facebook.display = "page" + # config.facebook.api_version = "v2.3" + # config.facebook.parse = :json + # + # config.instagram.key = "" + # config.instagram.secret = "" + # config.instagram.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=instagram" + # config.instagram.user_info_mapping = {:email => "username"} + # config.instagram.access_permissions = ["basic", "public_content", "follower_list", "comments", "relationships", "likes"] + # + # config.github.key = "" + # config.github.secret = "" + # config.github.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=github" + # config.github.user_info_mapping = {:email => "name"} + # config.github.scope = "" + # + # config.paypal.key = "" + # config.paypal.secret = "" + # config.paypal.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=paypal" + # config.paypal.user_info_mapping = {:email => "email"} + # + # config.wechat.key = "" + # config.wechat.secret = "" + # config.wechat.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=wechat" + # + # For Auth0, site is required and should match the domain provided by Auth0. + # + # config.auth0.key = "" + # config.auth0.secret = "" + # config.auth0.callback_url = "https://0.0.0.0:3000/oauth/callback?provider=auth0" + # config.auth0.site = "https://example.auth0.com" + # + config.google.key = ENV['GOOGLE_CLIENT_ID'] + config.google.secret = ENV['GOOGLE_CLIENT_SECRET'] + config.google.callback_url = "http://localhost:3000/oauth/callback?provider=google" + config.google.user_info_mapping = {:email => "email", :name => "name", :google_id => "id"} + config.google.scope = "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/spreadsheets" + config.google.auth_url = '/o/oauth2/auth?access_type=offline&include_granted_scopes=true' + # + # For Microsoft Graph, the key will be your App ID, and the secret will be your app password/public key. + # The callback URL "can't contain a query string or invalid special characters" + # See: https://docs.microsoft.com/en-us/azure/active-directory/active-directory-v2-limitations#restrictions-on-redirect-uris + # More information at https://graph.microsoft.io/en-us/docs + # + # config.microsoft.key = "" + # config.microsoft.secret = "" + # config.microsoft.callback_url = "http://0.0.0.0:3000/oauth/callback/microsoft" + # config.microsoft.user_info_mapping = {:email => "userPrincipalName", :username => "displayName"} + # config.microsoft.scope = "openid email https://graph.microsoft.com/User.Read" + # + # config.vk.key = "" + # config.vk.secret = "" + # config.vk.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=vk" + # config.vk.user_info_mapping = {:login => "domain", :name => "full_name"} + # config.vk.api_version = "5.71" + # + # config.slack.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=slack" + # config.slack.key = '' + # config.slack.secret = '' + # config.slack.user_info_mapping = {email: 'email'} + # + # To use liveid in development mode you have to replace mydomain.com with + # a valid domain even in development. To use a valid domain in development + # simply add your domain in your /etc/hosts file in front of 127.0.0.1 + # + # config.liveid.key = "" + # config.liveid.secret = "" + # config.liveid.callback_url = "http://mydomain.com:3000/oauth/callback?provider=liveid" + # config.liveid.user_info_mapping = {:username => "name"} + + # For information about JIRA API: + # https://developer.atlassian.com/display/JIRADEV/JIRA+REST+API+Example+-+OAuth+authentication + # To obtain the consumer key and the public key you can use the jira-ruby gem https://github.com/sumoheavy/jira-ruby + # or run openssl req -x509 -nodes -newkey rsa:1024 -sha1 -keyout rsakey.pem -out rsacert.pem to obtain the public key + # Make sure you have configured the application link properly + + # config.jira.key = "1234567" + # config.jira.secret = "jiraTest" + # config.jira.site = "http://localhost:2990/jira/plugins/servlet/oauth" + # config.jira.signature_method = "RSA-SHA1" + # config.jira.private_key_file = "rsakey.pem" + + # For information about Salesforce API: + # https://developer.salesforce.com/signup & + # https://www.salesforce.com/us/developer/docs/api_rest/ + # Salesforce callback_url must be https. You can run the following to generate self-signed ssl cert: + # openssl req -new -newkey rsa:2048 -sha1 -days 365 -nodes -x509 -keyout server.key -out server.crt + # Make sure you have configured the application link properly + # config.salesforce.key = '123123' + # config.salesforce.secret = 'acb123' + # config.salesforce.callback_url = "https://127.0.0.1:9292/oauth/callback?provider=salesforce" + # config.salesforce.scope = "full" + # config.salesforce.user_info_mapping = {:email => "email"} + + # config.line.key = "" + # config.line.secret = "" + # config.line.callback_url = "http://mydomain.com:3000/oauth/callback?provider=line" + + # --- user config --- + config.user_config do |user| + # -- core -- + # Specify username attributes, for example: [:username, :email]. + # Default: `[:email]` + # + # user.username_attribute_names = + + # Change *virtual* password attribute, the one which is used until an encrypted one is generated. + # Default: `:password` + # + # user.password_attribute_name = + + # Downcase the username before trying to authenticate, default is false + # Default: `false` + # + # user.downcase_username_before_authenticating = + + # Change default email attribute. + # Default: `:email` + # + # user.email_attribute_name = + + # Change default crypted_password attribute. + # Default: `:crypted_password` + # + # user.crypted_password_attribute_name = + + # What pattern to use to join the password with the salt + # Default: `""` + # + # user.salt_join_token = + + # Change default salt attribute. + # Default: `:salt` + # + # user.salt_attribute_name = + + # How many times to apply encryption to the password. + # Default: 1 in test env, `nil` otherwise + # + user.stretches = 1 if Rails.env.test? + + # Encryption key used to encrypt reversible encryptions such as AES256. + # WARNING: If used for users' passwords, changing this key will leave passwords undecryptable! + # Default: `nil` + # + # user.encryption_key = + + # Use an external encryption class. + # Default: `nil` + # + # user.custom_encryption_provider = + + # Encryption algorithm name. See 'encryption_algorithm=' for available options. + # Default: `:bcrypt` + # + # user.encryption_algorithm = + + # Make this configuration inheritable for subclasses. Useful for ActiveRecord's STI. + # Default: `false` + # + # user.subclasses_inherit_config = + + # -- remember_me -- + # How long in seconds the session length will be + # Default: `60 * 60 * 24 * 7` + # + # user.remember_me_for = + + # When true, sorcery will persist a single remember me token for all + # logins/logouts (to support remembering on multiple browsers simultaneously). + # Default: false + # + # user.remember_me_token_persist_globally = + + # -- user_activation -- + # The attribute name to hold activation state (active/pending). + # Default: `:activation_state` + # + # user.activation_state_attribute_name = + + # The attribute name to hold activation code (sent by email). + # Default: `:activation_token` + # + # user.activation_token_attribute_name = + + # The attribute name to hold activation code expiration date. + # Default: `:activation_token_expires_at` + # + # user.activation_token_expires_at_attribute_name = + + # How many seconds before the activation code expires. nil for never expires. + # Default: `nil` + # + # user.activation_token_expiration_period = + + # REQUIRED: + # User activation mailer class. + # Default: `nil` + # + # user.user_activation_mailer = + + # When true, sorcery will not automatically + # send the activation details email, and allow you to + # manually handle how and when the email is sent. + # Default: `false` + # + # user.activation_mailer_disabled = + + # Method to send email related + # options: `:deliver_later`, `:deliver_now`, `:deliver` + # Default: :deliver (Rails version < 4.2) or :deliver_now (Rails version 4.2+) + # + # user.email_delivery_method = + + # Activation needed email method on your mailer class. + # Default: `:activation_needed_email` + # + # user.activation_needed_email_method_name = + + # Activation success email method on your mailer class. + # Default: `:activation_success_email` + # + # user.activation_success_email_method_name = + + # Do you want to prevent users who did not activate by email from logging in? + # Default: `true` + # + # user.prevent_non_active_users_to_login = + + # -- reset_password -- + # Password reset token attribute name. + # Default: `:reset_password_token` + # + # user.reset_password_token_attribute_name = + + # Password token expiry attribute name. + # Default: `:reset_password_token_expires_at` + # + # user.reset_password_token_expires_at_attribute_name = + + # When was password reset email sent. Used for hammering protection. + # Default: `:reset_password_email_sent_at` + # + # user.reset_password_email_sent_at_attribute_name = + + # REQUIRED: + # Password reset mailer class. + # Default: `nil` + # + # user.reset_password_mailer = + + # Reset password email method on your mailer class. + # Default: `:reset_password_email` + # + # user.reset_password_email_method_name = + + # When true, sorcery will not automatically + # send the password reset details email, and allow you to + # manually handle how and when the email is sent + # Default: `false` + # + # user.reset_password_mailer_disabled = + + # How many seconds before the reset request expires. nil for never expires. + # Default: `nil` + # + # user.reset_password_expiration_period = + + # Hammering protection: how long in seconds to wait before allowing another email to be sent. + # Default: `5 * 60` + # + # user.reset_password_time_between_emails = + + # Access counter to a reset password page attribute name + # Default: `:access_count_to_reset_password_page` + # + # user.reset_password_page_access_count_attribute_name = + + # -- magic_login -- + # Magic login code attribute name. + # Default: `:magic_login_token` + # + # user.magic_login_token_attribute_name = + + # Magic login expiry attribute name. + # Default: `:magic_login_token_expires_at` + # + # user.magic_login_token_expires_at_attribute_name = + + # When was magic login email sent — used for hammering protection. + # Default: `:magic_login_email_sent_at` + # + # user.magic_login_email_sent_at_attribute_name = + + # REQUIRED: + # Magic login mailer class. + # Default: `nil` + # + # user.magic_login_mailer_class = + + # Magic login email method on your mailer class. + # Default: `:magic_login_email` + # + # user.magic_login_email_method_name = + + # When true, sorcery will not automatically + # send magic login details email, and allow you to + # manually handle how and when the email is sent + # Default: `true` + # + # user.magic_login_mailer_disabled = + + # How many seconds before the request expires. nil for never expires. + # Default: `nil` + # + # user.magic_login_expiration_period = + + # Hammering protection: how long in seconds to wait before allowing another email to be sent. + # Default: `5 * 60` + # + # user.magic_login_time_between_emails = + + # -- brute_force_protection -- + # Failed logins attribute name. + # Default: `:failed_logins_count` + # + # user.failed_logins_count_attribute_name = + + # This field indicates whether user is banned and when it will be active again. + # Default: `:lock_expires_at` + # + # user.lock_expires_at_attribute_name = + + # How many failed logins are allowed. + # Default: `50` + # + # user.consecutive_login_retries_amount_limit = + + # How long the user should be banned, in seconds. 0 for permanent. + # Default: `60 * 60` + # + # user.login_lock_time_period = + + # Unlock token attribute name + # Default: `:unlock_token` + # + # user.unlock_token_attribute_name = + + # Unlock token mailer method + # Default: `:send_unlock_token_email` + # + # user.unlock_token_email_method_name = + + # When true, sorcery will not automatically + # send email with the unlock token + # Default: `false` + # + # user.unlock_token_mailer_disabled = true + + # REQUIRED: + # Unlock token mailer class. + # Default: `nil` + # + # user.unlock_token_mailer = + + # -- activity logging -- + # Last login attribute name. + # Default: `:last_login_at` + # + # user.last_login_at_attribute_name = + + # Last logout attribute name. + # Default: `:last_logout_at` + # + # user.last_logout_at_attribute_name = + + # Last activity attribute name. + # Default: `:last_activity_at` + # + # user.last_activity_at_attribute_name = + + # How long since user's last activity will they be considered logged out? + # Default: `10 * 60` + # + # user.activity_timeout = + + # -- external -- + # Class which holds the various external provider data for this user. + # Default: `nil` + # + user.authentications_class = Authentication + + # User's identifier in the `authentications` class. + # Default: `:user_id` + # + # user.authentications_user_id_attribute_name = + + # Provider's identifier in the `authentications` class. + # Default: `:provider` + # + # user.provider_attribute_name = + + # User's external unique identifier in the `authentications` class. + # Default: `:uid` + # + # user.provider_uid_attribute_name = + end + + # This line must come after the 'user config' block. + # Define which model authenticates with sorcery. + config.user_class = "User" +end diff --git a/config/routes.rb b/config/routes.rb index 5bc2fc6..dfdec49 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,9 +13,14 @@ Rails.application.routes.draw do # short URL for form page get '/s/:id/form' => 'forms#form', as: :form_submitter + get 'oauth/callback', to: 'oauths#callback' + get 'oauth/:provider', to: 'oauths#oauth', as: :auth_at_provider + + get '/signup' => 'sessions#new', as: :signup # TODO: add proper signup page get '/login' => 'sessions#new', as: :login get '/logout' => 'sessions#destroy', as: :logout get '/auth' => 'sessions#auth', as: :auth + get '/demo' => 'home#demo', as: :demo root 'home#index' end diff --git a/db/migrate/20200412165834_add_sequential_id_to_submissions.rb b/db/migrate/20200412165834_add_sequential_id_to_submissions.rb new file mode 100644 index 0000000..5b9c899 --- /dev/null +++ b/db/migrate/20200412165834_add_sequential_id_to_submissions.rb @@ -0,0 +1,5 @@ +class AddSequentialIdToSubmissions < ActiveRecord::Migration[6.0] + def change + add_column :submissions, :sequential_id, :integer + end +end diff --git a/db/migrate/20200412214304_add_token_to_attachments.rb b/db/migrate/20200412214304_add_token_to_attachments.rb new file mode 100644 index 0000000..76922bc --- /dev/null +++ b/db/migrate/20200412214304_add_token_to_attachments.rb @@ -0,0 +1,6 @@ +class AddTokenToAttachments < ActiveRecord::Migration[6.0] + def change + add_column :active_storage_attachments, :token, :string + add_index :active_storage_attachments, :token, unique: true + end +end diff --git a/db/migrate/20200413101920_sorcery_core.rb b/db/migrate/20200413101920_sorcery_core.rb new file mode 100644 index 0000000..9100c7e --- /dev/null +++ b/db/migrate/20200413101920_sorcery_core.rb @@ -0,0 +1,15 @@ +class SorceryCore < ActiveRecord::Migration[6.0] + def change + add_column :users, :crypted_password, :string + add_column :users, :salt, :string + add_column :users, :magic_login_token, :string + add_column :users, :magic_login_token_expires_at, :datetime + add_column :users, :magic_login_email_sent_at, :datetime + + add_column :authentications, :provider, :string, null: false + add_column :authentications, :uid, :string, null: false + + add_index :users, :magic_login_token + add_index :users, :email, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 4e875ea..153d317 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.define(version: 2020_04_09_001610) do +ActiveRecord::Schema.define(version: 2020_04_13_101920) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -21,8 +21,10 @@ ActiveRecord::Schema.define(version: 2020_04_09_001610) do t.bigint "record_id", null: false t.bigint "blob_id", null: false t.datetime "created_at", null: false + t.string "token" t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + t.index ["token"], name: "index_active_storage_attachments_on_token", unique: true end create_table "active_storage_blobs", force: :cascade do |t| @@ -43,6 +45,8 @@ ActiveRecord::Schema.define(version: 2020_04_09_001610) do t.datetime "updated_at", precision: 6, null: false t.text "access_token_ciphertext" t.text "refresh_token_ciphertext" + t.string "provider" + t.string "uid" end create_table "forms", force: :cascade do |t| @@ -63,6 +67,7 @@ ActiveRecord::Schema.define(version: 2020_04_09_001610) do t.datetime "updated_at", precision: 6, null: false t.string "remote_ip" t.string "referrer" + t.integer "sequential_id" end create_table "users", force: :cascade do |t| @@ -71,6 +76,13 @@ ActiveRecord::Schema.define(version: 2020_04_09_001610) do t.string "google_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.string "crypted_password" + t.string "salt" + t.string "magic_login_token" + t.datetime "magic_login_token_expires_at" + t.datetime "magic_login_email_sent_at" + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["magic_login_token"], name: "index_users_on_magic_login_token" end add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"