diff --git a/Gemfile b/Gemfile index 7bb22c1..6879503 100644 --- a/Gemfile +++ b/Gemfile @@ -39,6 +39,9 @@ gem 'net-smtp', require: false gem 'net-imap', require: false gem 'net-pop', require: false gem "apipie-rails", "~> 1.2.0" +gem 'omniauth', '>=2.0.0' +gem 'omniauth-rails_csrf_protection' +gem 'omniauth-tara', github: 'internetee/omniauth-tara' # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible # gem 'rack-cors' diff --git a/app/controllers/auth/tara_controller.rb b/app/controllers/auth/tara_controller.rb new file mode 100644 index 0000000..a9d83fa --- /dev/null +++ b/app/controllers/auth/tara_controller.rb @@ -0,0 +1,51 @@ +# rubocop:disable Metrics + +module Auth + class TaraController < ParentController + allow_unauthenticated + + def callback + expires_now + + unless in_white_list? + flash[:alert] = I18n.t('.access_denied') + redirect_to sign_in_path, status: :see_other and return + end + + session[:omniauth_hash] = user_hash.delete_if { |key, _| key == 'credentials' } + @user = User.from_omniauth(user_hash) + @user.save! && @user.reload + + @app_session = create_app_session + + if @app_session + log_in @app_session + set_current_session + + redirect_to root_path, status: :see_other + else + flash[:alert] = I18n.t('.incorrect_details') + render 'dashboard/index', status: :unprocessable_entity + end + end + + private + + def in_white_list? + WhiteCode.find_by(code: user_hash['uid'].slice(2..-1)).present? + end + + def set_current_session + Current.user = @user + flash[:notice] = I18n.t('.success') + end + + def user_hash + request.env['omniauth.auth'] + end + + def create_app_session + @user.app_sessions.create + end + end +end \ No newline at end of file diff --git a/app/controllers/concerns/authenticate.rb b/app/controllers/concerns/authenticate.rb new file mode 100644 index 0000000..440c000 --- /dev/null +++ b/app/controllers/concerns/authenticate.rb @@ -0,0 +1,72 @@ +module Authenticate + extend ActiveSupport::Concern + + included do + before_action :authenticate + before_action :need_to_login, unless: :logged_in? + + helper_method :logged_in? + helper_method :current_user + end + + class_methods do + def skip_authentication(**options) + skip_before_action :authenticate, options + skip_before_action :need_to_login, options + end + + def allow_unauthenticated(**options) + skip_before_action :need_to_login, options + end + end + + protected + + def log_in(app_session, remember_me: false) + if remember_me + cookies.encrypted.permanent[:app_session] = { + value: app_session.to_h + } + else + cookies.signed[:app_session] = { + value: app_session.to_h, + expires: 1.day + } + end + end + + def logout + Current&.app_session&.destroy + end + + def logged_in? + Current.user.present? + end + + def current_user + Current.user + end + + private + + def need_to_login + flash[:notice] = t('login_required') + render 'sessions/new', status: :unauthorized + end + + def authenticate + cookie = cookies.encrypted[:app_session]&.with_indifferent_access + cookie = cookies.signed[:app_session]&.with_indifferent_access if cookie.nil? + + return nil if cookie.nil? + + user = User.find(cookie[:user_id]) + app_session = user&.authenticate_session_token(cookie[:app_session], cookie[:token]) + + Current.user = app_session&.user + Current.app_session = app_session + rescue NoMatchingPatternError, ActiveRecord::RecordNotFound + Current.user = nil + Current.app_session = nil + end +end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 37e6dff..5e5dd00 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,6 +1,4 @@ class DashboardController < ParentController - before_action :require_user_logged_in! - def index @pagy, @invoices = pagy(Invoice.search(params), items: params[:per_page] ||= 25, diff --git a/app/controllers/dashboards/invoice_status_controller.rb b/app/controllers/dashboards/invoice_status_controller.rb index c3b231b..6d27140 100644 --- a/app/controllers/dashboards/invoice_status_controller.rb +++ b/app/controllers/dashboards/invoice_status_controller.rb @@ -1,6 +1,4 @@ class Dashboards::InvoiceStatusController < ParentController - before_action :require_user_logged_in! - def update @invoice = Invoice.find(params[:id]) temporary_unavailable and return unless @invoice.registry? diff --git a/app/controllers/everypay_controller.rb b/app/controllers/everypay_controller.rb index 4ea74e8..8578be0 100644 --- a/app/controllers/everypay_controller.rb +++ b/app/controllers/everypay_controller.rb @@ -1,6 +1,4 @@ class EverypayController < ParentController - before_action :require_user_logged_in! - def index; end def everypay_data diff --git a/app/controllers/invoice_details/descriptions_controller.rb b/app/controllers/invoice_details/descriptions_controller.rb index a8c0650..f2f9b7e 100644 --- a/app/controllers/invoice_details/descriptions_controller.rb +++ b/app/controllers/invoice_details/descriptions_controller.rb @@ -1,6 +1,4 @@ class InvoiceDetails::DescriptionsController < ParentController - before_action :require_user_logged_in! - def show @description = Invoice.find(params[:id])&.description end diff --git a/app/controllers/invoice_details/directo_controller.rb b/app/controllers/invoice_details/directo_controller.rb index 9f2e588..572d4c7 100644 --- a/app/controllers/invoice_details/directo_controller.rb +++ b/app/controllers/invoice_details/directo_controller.rb @@ -1,8 +1,5 @@ class InvoiceDetails::DirectoController < ParentController require 'rexml/document' - - before_action :require_user_logged_in! - def show directo = Invoice.find(params[:id]) diff --git a/app/controllers/invoice_details/everypay_response_controller.rb b/app/controllers/invoice_details/everypay_response_controller.rb index 5ecd775..031f4df 100644 --- a/app/controllers/invoice_details/everypay_response_controller.rb +++ b/app/controllers/invoice_details/everypay_response_controller.rb @@ -1,6 +1,4 @@ class InvoiceDetails::EverypayResponseController < ParentController - before_action :require_user_logged_in! - def show everypay = Invoice.find(params[:id]) @everypay = everypay.everypay_response diff --git a/app/controllers/invoice_details/payment_references_controller.rb b/app/controllers/invoice_details/payment_references_controller.rb index 9390352..9840d13 100644 --- a/app/controllers/invoice_details/payment_references_controller.rb +++ b/app/controllers/invoice_details/payment_references_controller.rb @@ -1,6 +1,4 @@ class InvoiceDetails::PaymentReferencesController < ParentController - before_action :require_user_logged_in! - def show @payment_reference = Invoice.find(params[:id])&.payment_reference end diff --git a/app/controllers/parent_controller.rb b/app/controllers/parent_controller.rb index 73e2d52..78e54df 100644 --- a/app/controllers/parent_controller.rb +++ b/app/controllers/parent_controller.rb @@ -3,25 +3,14 @@ class ParentController < ActionController::Base include AbstractController::Rendering include ActionView::Layouts - # append_view_path "#{Rails.root}/app/views/layouts" - layout "application" + include Authenticate + + layout 'application' skip_before_action :verify_authenticity_token helper_method :turbo_frame_request? - before_action :set_current_user - def render_turbo_flash turbo_stream.update('flash', partial: 'shared/flash') end - - def set_current_user - # finds user with session data and stores it if present - Current.user = User.find_by(id: session[:user_id]) if session[:user_id] - end - - def require_user_logged_in! - # allows only logged in user - redirect_to sign_in_path, alert: 'You must be signed in', turbolinks: false if Current.user.nil? - end end diff --git a/app/controllers/references_controller.rb b/app/controllers/references_controller.rb index d85cd72..196b39f 100644 --- a/app/controllers/references_controller.rb +++ b/app/controllers/references_controller.rb @@ -1,6 +1,4 @@ class ReferencesController < ParentController - before_action :require_user_logged_in! - def index @pagy, @references = pagy(Reference.search(params), items: params[:per_page] ||= 25, link_extra: 'data-turbo-action="advance"') diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb deleted file mode 100644 index b104d73..0000000 --- a/app/controllers/registrations_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -class RegistrationsController < ParentController - before_action :require_user_logged_in! - - def new - @user = User.new - end - - def create - @user = User.new(user_params) - @new_user = User.new - - if @user.save - flash[:notice] = 'Successfully created account' - - respond_to do |format| - format.turbo_stream do - render turbo_stream: [ - turbo_stream.prepend('users', partial: 'users/user', locals: { user: @user }), - turbo_stream.update('new_user', partial: 'users/form', - locals: { user: @new_user, url: sign_up_path }), - ] - end - end - else - render :new - end - end - - private - - def user_params - params.require(:user).permit(:email, :password, :password_confirmation) - end -end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 3d3d511..b20c907 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,19 +1,23 @@ class SessionsController < ParentController + allow_unauthenticated + def new; end - def create - user = User.find_by(email: params[:email]) - if user.present? && user.authenticate(params[:password]) - session[:user_id] = user.id - redirect_to root_path, status: :see_other, flash: { notice: 'Logged in successfully' } - else - flash.now[:alert] = 'Wrong username/password' - render :new, status: :unprocessable_entity - end - end + # def create + # user = User.find_by(email: params[:email]) + # if user.present? && user.authenticate(params[:password]) + # session[:user_id] = user.id + # redirect_to root_path, status: :see_other, flash: { notice: 'Logged in successfully' } + # else + # flash.now[:alert] = 'Wrong username/password' + # render :new, status: :unprocessable_entity + # end + # end def destroy - session[:user_id] = nil - redirect_to sign_in_path, flash: { notice: 'Logged out' } + logout + + flash[:success] = t('.success') + redirect_to root_path, status: :see_other end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb deleted file mode 100644 index 286bf63..0000000 --- a/app/controllers/users_controller.rb +++ /dev/null @@ -1,84 +0,0 @@ -class UsersController < ParentController - before_action :require_user_logged_in! - - before_action :set_admin, only: %i[edit update destroy] - - def index - @users = User.all - - @new_user = User.new - end - - def new; end - - def edit - return if @user != Current.user - end - - def update - respond_to do |format| - if Current.user.update(strong_params) - flash.now[:notice] = 'Admin user was updated' - format.turbo_stream do - render turbo_stream: [ - render_turbo_flash, - turbo_stream.replace(Current.user, - partial: 'users/user', - locals: { user: Current.user }), - ] - end - else - format.turbo_stream do - flash.now[:alert] = 'Something went wrong!' - render turbo_stream: [ - render_turbo_flash, - ] - end - end - end - end - - def destroy - title = @user.email - respond_to do |format| - if @user.destroy - flash.now[:alert] = "#{title} was deleted" - format.turbo_stream do - render turbo_stream: [ - turbo_stream.remove(@user), - ] - end - else - format.turbo_stream do - flash.now[:alert] = 'Something went wrong!' - render turbo_stream: [] - end - end - end - end - - def search - redirect_to users_path and return unless params[:title_search].present? - - @search_param = params[:title_search].to_s.downcase - @users = User.search_by_email(@search_param).with_pg_search_highlight - - respond_to do |format| - format.turbo_stream do - render turbo_stream: [ - turbo_stream.update('users', partial: 'users/users', locals: { users: @users }), - ] - end - end - end - - private - - def strong_params - params.require(:user).permit(:email, :password, :password_confirmation) - end - - def set_admin - @user = User.find(params[:id]) - end -end diff --git a/app/controllers/white_codes_controller.rb b/app/controllers/white_codes_controller.rb new file mode 100644 index 0000000..9b931b7 --- /dev/null +++ b/app/controllers/white_codes_controller.rb @@ -0,0 +1,93 @@ +# rubocop:disable Metrics +class WhiteCodesController < ParentController + before_action :set_white_code, only: %i[edit update destroy] + + def index + @white_codes = WhiteCode.all + @new_white_code = WhiteCode.new + end + + def new + @white_code = WhiteCode.new + end + + def edit; end + + def create + @white_code = WhiteCode.new(strong_params) + + respond_to do |format| + if @white_code.save + flash.now[:notice] = 'Identity code added to white list' + @white_code_new = WhiteCode.new + + format.turbo_stream do + render turbo_stream: [ + render_turbo_flash, + turbo_stream.append('white_codes', partial: 'white_codes/white_code', locals: { white_code: @white_code }), + turbo_stream.replace('admin_form', partial: 'white_codes/form', locals: { white_code: @white_code_new, url: white_codes_path }) + ] + end + else + format.turbo_stream do + flash.now[:alert] = @white_code.errors.full_messages.join(', ') + render turbo_stream: [render_turbo_flash] + end + end + end + end + + def update + respond_to do |format| + if @white_code.update(strong_params) + flash.now[:notice] = 'Identity code in white list was updated' + @white_code_new = WhiteCode.new + + format.turbo_stream do + render turbo_stream: [ + render_turbo_flash, + turbo_stream.replace(@white_code, partial: 'white_codes/white_code', locals: { white_code: @white_code }), + turbo_stream.replace('admin_form', partial: 'white_codes/form', locals: { white_code: @white_code_new, url: white_codes_path }) + ] + end + else + format.turbo_stream do + flash.now[:alert] = @white_code.errors.full_messages.join(', ') + render turbo_stream: [render_turbo_flash] + end + end + end + end + + def destroy + code = @white_code.code + respond_to do |format| + if @white_code.destroy + flash.now[:alert] = "#{code} was deleted from white list" + format.turbo_stream do + render turbo_stream: [ + render_turbo_flash, + turbo_stream.remove(@white_code) + ] + end + else + format.turbo_stream do + flash.now[:alert] = @white_code.errors.full_messages.join(', ') + render turbo_stream: [ + render_turbo_flash + ] + end + end + end + end + + private + + def strong_params + params.require(:white_code).permit(:code) + end + + def set_white_code + @white_code = WhiteCode.find(params[:id]) + end +end diff --git a/app/models/app_session.rb b/app/models/app_session.rb new file mode 100644 index 0000000..08ac617 --- /dev/null +++ b/app/models/app_session.rb @@ -0,0 +1,17 @@ +class AppSession < ApplicationRecord + belongs_to :user + + has_secure_password :token, validations: false + + before_create do + self.token = self.class.generate_unique_secure_token + end + + def to_h + { + user_id: user.id, + app_session: id, + token: + } + end +end diff --git a/app/models/current.rb b/app/models/current.rb index 61ca520..df3e894 100644 --- a/app/models/current.rb +++ b/app/models/current.rb @@ -1,4 +1,3 @@ class Current < ActiveSupport::CurrentAttributes - # makes Current.user accessible in view files. - attribute :user + attribute :user, :app_session end diff --git a/app/models/user.rb b/app/models/user.rb index c5a5921..b7caf81 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,10 +1,11 @@ class User < ApplicationRecord include PgSearch::Model + include Authentication - has_secure_password + has_secure_password :password, validations: false - validates :email, presence: true, uniqueness: true, - format: { with: /\A[^@\s]+@[^@\s]+\z/, message: 'Invalid email' } + validates :email, format: { with: /\A[^@\s]+@[^@\s]+\z/, message: 'Invalid email' }, allow_nil: true + has_many :app_sessions, dependent: :destroy pg_search_scope :search_by_email, against: [:email], @@ -13,4 +14,16 @@ class User < ApplicationRecord prefix: true } } + + def self.from_omniauth(omniauth_hash) + uid = omniauth_hash['uid'] + provider = omniauth_hash['provider'] + + user = User.find_or_initialize_by(identity_code: uid.slice(2..-1)) + user.provider = provider + user.uid = uid + user.identity_code = uid.slice(2..-1) + + user + end end diff --git a/app/models/user/authentication.rb b/app/models/user/authentication.rb new file mode 100644 index 0000000..c1e6602 --- /dev/null +++ b/app/models/user/authentication.rb @@ -0,0 +1,9 @@ +module User::Authentication + extend ActiveSupport::Concern + + def authenticate_session_token(app_session_id, token) + app_sessions.find(app_session_id).authenticate_token(token) + rescue ActiveRecord::RecordNotFound + nil + end +end diff --git a/app/models/white_code.rb b/app/models/white_code.rb new file mode 100644 index 0000000..91a5b1a --- /dev/null +++ b/app/models/white_code.rb @@ -0,0 +1,3 @@ +class WhiteCode < ApplicationRecord + validates :code, presence: true, uniqueness: true, length: { is: 11 } +end diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index 377cf62..ecde9a3 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -4,18 +4,7 @@
Sign In
- <%= button_to 'New admin user', '#', class: 'h-10 py-2 my-1 px-4 border border-transparent text-sm font-semibold rounded-md text-white bg-violet-700 hover:bg-violet-800 transition duration-150 ease-in-out shadow-md cursor-pointer', data: { turbo_frame: 'admin_form' } %> - | -
---|
Code | ++ <%= link_to 'New identity code to white list', new_white_code_path, class: 'h-10 py-2 my-1 px-4 border border-transparent text-sm font-semibold rounded-md text-white bg-violet-700 hover:bg-violet-800 transition duration-150 ease-in-out shadow-md cursor-pointer', data: { turbo_frame: 'admin_form' } %> + | +
---|