From fbf692383581bf56af1718024ed4037ec35d90f0 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 22 Sep 2024 21:57:05 +0200 Subject: [PATCH] Add auth and ruby update --- .ruby-version | 2 +- Dockerfile | 2 +- Gemfile | 3 +- Gemfile.lock | 28 +++- app/controllers/application_controller.rb | 28 +++- app/controllers/backoffice_controller.rb | 6 + app/controllers/checklists_controller.rb | 2 + app/controllers/checks_controller.rb | 2 + app/controllers/concerns/backoffice_menu.rb | 12 ++ app/controllers/home_controller.rb | 4 + app/controllers/link_categories_controller.rb | 2 + app/controllers/links_controller.rb | 2 + app/controllers/rodauth_controller.rb | 37 +++++ app/helpers/backoffice_helper.rb | 2 + app/helpers/public_helper.rb | 2 + app/misc/rodauth_app.rb | 25 +++ app/misc/rodauth_main.rb | 154 ++++++++++++++++++ app/models/account.rb | 5 + app/views/backoffice/show.html.erb | 24 +++ app/views/home/root.html.erb | 2 + app/views/home/show.html.erb | 21 --- app/views/layouts/_flash.html.erb | 2 +- app/views/layouts/_navigation.html.erb | 21 ++- app/views/layouts/_sidebar.html.erb | 27 +++ app/views/layouts/application.html.erb | 19 ++- app/views/rodauth/_login_form.html.erb | 26 +++ app/views/rodauth/_login_form_footer.html.erb | 9 + app/views/rodauth/change_password.html.erb | 28 ++++ app/views/rodauth/login.html.erb | 3 + app/views/rodauth/logout.html.erb | 14 ++ app/views/rodauth/multi_phase_login.html.erb | 3 + app/views/rodauth/profile.html.erb | 3 + app/views/rodauth/remember.html.erb | 22 +++ config/initializers/rodauth.rb | 3 + config/locales/activerecord.yml | 6 + config/locales/i11yist.yml | 3 +- config/routes.rb | 34 ++-- db/migrate/20240922172043_create_rodauth.rb | 45 +++++ db/schema.rb | 15 +- db/seeds.rb | 4 +- .../controllers/backoffice_controller_test.rb | 8 + test/controllers/public_controller_test.rb | 8 + test/fixtures/accounts.yml | 10 ++ 43 files changed, 614 insertions(+), 64 deletions(-) create mode 100644 app/controllers/backoffice_controller.rb create mode 100644 app/controllers/concerns/backoffice_menu.rb create mode 100644 app/controllers/rodauth_controller.rb create mode 100644 app/helpers/backoffice_helper.rb create mode 100644 app/helpers/public_helper.rb create mode 100644 app/misc/rodauth_app.rb create mode 100644 app/misc/rodauth_main.rb create mode 100644 app/models/account.rb create mode 100644 app/views/backoffice/show.html.erb create mode 100644 app/views/home/root.html.erb create mode 100644 app/views/rodauth/_login_form.html.erb create mode 100644 app/views/rodauth/_login_form_footer.html.erb create mode 100644 app/views/rodauth/change_password.html.erb create mode 100644 app/views/rodauth/login.html.erb create mode 100644 app/views/rodauth/logout.html.erb create mode 100644 app/views/rodauth/multi_phase_login.html.erb create mode 100644 app/views/rodauth/profile.html.erb create mode 100644 app/views/rodauth/remember.html.erb create mode 100644 config/initializers/rodauth.rb create mode 100644 db/migrate/20240922172043_create_rodauth.rb create mode 100644 test/controllers/backoffice_controller_test.rb create mode 100644 test/controllers/public_controller_test.rb create mode 100644 test/fixtures/accounts.yml diff --git a/.ruby-version b/.ruby-version index 6d5369b..f13c6f4 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-3.3.4 +ruby-3.3.5 diff --git a/Dockerfile b/Dockerfile index 949012b..64b137a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ARG UID=1000 ARG GID=1000 ARG APP_PORT=3000 ARG INSTALL_DIR=/${NAME} -ARG RUBY_VERSION=3.3.4 +ARG RUBY_VERSION=3.3.5 FROM ruby:${RUBY_VERSION} AS development ARG NAME diff --git a/Gemfile b/Gemfile index fb12547..cd7a44b 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source "https://rubygems.org" -ruby "3.3.4" +ruby "3.3.5" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" gem "rails", "~> 7.2" @@ -58,6 +58,7 @@ gem "pagy", "~> 9.0" gem "pandoc-ruby" gem "prawn-markup" gem "prawn-rails" +gem "rodauth-rails" gem "sablon" gem "slim" diff --git a/Gemfile.lock b/Gemfile.lock index 975d209..551309d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,8 +76,12 @@ GEM tzinfo (~> 2.0, >= 2.0.5) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) + after_commit_everywhere (1.4.0) + activerecord (>= 4.2) + activesupport ast (2.4.2) base64 (0.2.0) + bcrypt (3.1.20) bigdecimal (3.1.8) bindex (0.8.1) bootsnap (1.18.3) @@ -267,6 +271,21 @@ GEM io-console (~> 0.5) rexml (3.3.2) strscan + roda (3.84.0) + rack + rodauth (2.36.0) + roda (>= 2.6.0) + sequel (>= 4) + rodauth-model (0.2.1) + rodauth (~> 2.0) + rodauth-rails (1.15.0) + bcrypt + railties (>= 5.0, < 8) + roda (~> 3.76) + rodauth (~> 2.36) + rodauth-model (~> 0.2) + sequel-activerecord_connection (~> 1.1) + tilt rubocop (1.65.0) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -319,6 +338,12 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) + sequel (5.84.0) + bigdecimal + sequel-activerecord_connection (1.4.1) + activerecord (>= 5.0, < 8) + after_commit_everywhere (~> 1.1) + sequel (~> 5.38) slim (5.2.1) temple (~> 0.10.0) tilt (>= 2.1.0) @@ -396,6 +421,7 @@ DEPENDENCIES prawn-rails puma (>= 5.0) rails (~> 7.2) + rodauth-rails rubocop rubocop-capybara rubocop-rails @@ -413,7 +439,7 @@ DEPENDENCIES web-console RUBY VERSION - ruby 3.3.4p94 + ruby 3.3.5p100 BUNDLED WITH 2.5.15 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e99f4ce..a94b0a0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,6 +4,7 @@ class ApplicationController < ActionController::Base include Pagy::Backend # allow_browser versions: :modern + helper_method :sidebar? before_action :initialize_navbar @@ -12,15 +13,26 @@ class ApplicationController < ActionController::Base def initialize_navbar return unless request.get? - @navbar_items = [ - { label: "Dashboard", icon: :speedometer2, path: :root }, - { label: Report.model_name.human(count: 2), icon: :'journal-text', path: :reports }, - { label: Checklist.model_name.human(count: 2), icon: :'list-check', path: :checklists }, - { label: Check.model_name.human(count: 2), icon: :check2, path: :checks }, - { label: Link.model_name.human(count: 2), icon: :link, path: :links }, - { label: LinkCategory.model_name.human(count: 2), icon: :folder, path: :link_categories } - ] + @navbar_items = if rodauth.logged_in? + [ + { label: "Dashboard", icon: :speedometer2, path: :root }, + { label: Report.model_name.human(count: 2), icon: :'journal-text', path: :reports }, + { label: I18n.t("backoffice"), icon: :gear, path: :backoffice, active: %w[backoffice checklists checks links link_categories].include?(controller_name) }, + { label: Account.model_name.human, icon: :person, path: profile_path } + ] + else + [ { label: "Login", icon: :'door-closed', path: rodauth.login_path, label_class: "text-info" } ] + end @nav_path = controller_name @search_url = nil + @sidebar_items = initialize_sidebar_items + end + + def sidebar? + @sidebar_items && @sidebar_items.any? + end + + def initialize_sidebar_items + [] end end diff --git a/app/controllers/backoffice_controller.rb b/app/controllers/backoffice_controller.rb new file mode 100644 index 0000000..861e030 --- /dev/null +++ b/app/controllers/backoffice_controller.rb @@ -0,0 +1,6 @@ +class BackofficeController < ApplicationController + include BackofficeMenu + + def show + end +end diff --git a/app/controllers/checklists_controller.rb b/app/controllers/checklists_controller.rb index 89f1973..6243166 100644 --- a/app/controllers/checklists_controller.rb +++ b/app/controllers/checklists_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ChecklistsController < ApplicationController + include BackofficeMenu + before_action :set_checklist, only: %i[show edit update destroy] # GET /checklists diff --git a/app/controllers/checks_controller.rb b/app/controllers/checks_controller.rb index 1a36575..fce6a44 100644 --- a/app/controllers/checks_controller.rb +++ b/app/controllers/checks_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ChecksController < ApplicationController + include BackofficeMenu + before_action :set_check, only: %i[show edit update destroy] # GET /checks or /checks.json diff --git a/app/controllers/concerns/backoffice_menu.rb b/app/controllers/concerns/backoffice_menu.rb new file mode 100644 index 0000000..e2f72fb --- /dev/null +++ b/app/controllers/concerns/backoffice_menu.rb @@ -0,0 +1,12 @@ +module BackofficeMenu + extend ActiveSupport::Concern + + def initialize_sidebar_items + [ + { label: "Einstellungen", icon: :sliders, path: :backoffice }, + { label: Checklist.model_name.human(count: 2), icon: :'list-check', path: :checklists }, + { label: Check.model_name.human(count: 2), icon: :check2, path: :checks }, + { label: Link.model_name.human(count: 2), icon: :link, path: :links }, + { label: LinkCategory.model_name.human(count: 2), icon: :folder, path: :link_categories } ] + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 9ccfc38..f8ddff1 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -2,5 +2,9 @@ class HomeController < ApplicationController def show + if rodauth.logged_in? + else + render :root + end end end diff --git a/app/controllers/link_categories_controller.rb b/app/controllers/link_categories_controller.rb index cbccdf0..44b4fdc 100644 --- a/app/controllers/link_categories_controller.rb +++ b/app/controllers/link_categories_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class LinkCategoriesController < ApplicationController + include BackofficeMenu + before_action :set_link_category, only: %i[show edit update destroy] # GET /link_categories diff --git a/app/controllers/links_controller.rb b/app/controllers/links_controller.rb index 05f502c..8ceb58b 100644 --- a/app/controllers/links_controller.rb +++ b/app/controllers/links_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class LinksController < ApplicationController + include BackofficeMenu + before_action :set_link, only: %i[show edit update destroy] # GET /links diff --git a/app/controllers/rodauth_controller.rb b/app/controllers/rodauth_controller.rb new file mode 100644 index 0000000..b477d29 --- /dev/null +++ b/app/controllers/rodauth_controller.rb @@ -0,0 +1,37 @@ +class RodauthController < ApplicationController + # Used by Rodauth for rendering views, CSRF protection, running any + # registered action callbacks and rescue handlers, instrumentation etc. + + # Controller callbacks and rescue handlers will run around Rodauth endpoints. + # before_action :verify_captcha, only: :login, if: -> { request.post? } + # rescue_from("SomeError") { |exception| ... } + + # Layout can be changed for all Rodauth pages or only certain pages. + # layout "authentication" + # layout -> do + # case rodauth.current_route + # when :login, :create_account, :verify_account, :verify_account_resend, + # :reset_password, :reset_password_request + # "authentication" + # else + # "application" + # end + # end + # + before_action do + # Fix encoding in rodauth views. + response.headers["Content-Type"] = "text/html; charset=utf-8" if request.format.html? + end + + def profile + end + + private + def initialize_sidebar_items + [ + { label: "Profile", icon: :'person', path: profile_path, active: action_name == "profile" }, + { label: "Passwort ändern", icon: :'lock', path: rodauth.change_password_path, active: action_name == "change_password" }, + { label: "Logout", icon: :'box-arrow-right', path: rodauth.logout_path, active: action_name == "logout" } + ] + end +end diff --git a/app/helpers/backoffice_helper.rb b/app/helpers/backoffice_helper.rb new file mode 100644 index 0000000..f715f71 --- /dev/null +++ b/app/helpers/backoffice_helper.rb @@ -0,0 +1,2 @@ +module BackofficeHelper +end diff --git a/app/helpers/public_helper.rb b/app/helpers/public_helper.rb new file mode 100644 index 0000000..0d8e188 --- /dev/null +++ b/app/helpers/public_helper.rb @@ -0,0 +1,2 @@ +module PublicHelper +end diff --git a/app/misc/rodauth_app.rb b/app/misc/rodauth_app.rb new file mode 100644 index 0000000..6372422 --- /dev/null +++ b/app/misc/rodauth_app.rb @@ -0,0 +1,25 @@ +class RodauthApp < Rodauth::Rails::App + # primary configuration + configure RodauthMain + + # secondary configuration + # configure RodauthAdmin, :admin + + route do |r| + rodauth.load_memory # autologin remembered users + + r.rodauth # route rodauth requests + + # ==> Authenticating requests + # Call `rodauth.require_account` for requests that you want to + # require authentication for. For example: + # + # # authenticate /dashboard/* and /account/* requests + # if r.path.start_with?("/dashboard") || r.path.start_with?("/account") + # rodauth.require_account + # end + + # ==> Secondary configurations + # r.rodauth(:admin) # route admin rodauth requests + end +end diff --git a/app/misc/rodauth_main.rb b/app/misc/rodauth_main.rb new file mode 100644 index 0000000..fcfdc17 --- /dev/null +++ b/app/misc/rodauth_main.rb @@ -0,0 +1,154 @@ +require "sequel/core" + +class RodauthMain < Rodauth::Rails::Auth + configure do + # List of authentication features that are loaded. + # enable :create_account, :verify_account, :verify_account_grace_period, + # :login, :logout, :remember, + # :reset_password, :change_password, :change_login, :verify_login_change, + # :close_account + enable :login, :logout, :remember, :change_password + + # See the Rodauth documentation for the list of available config options: + # http://rodauth.jeremyevans.net/documentation.html + + # ==> General + # Initialize Sequel and have it reuse Active Record's database connection. + db Sequel.sqlite(extensions: :activerecord_connection, keep_reference: false) + # Avoid DB query that checks accounts table schema at boot time. + convert_token_id_to_integer? { Account.columns_hash["id"].type == :integer } + + # Change prefix of table and foreign key column names from default "account" + # accounts_table :users + # verify_account_table :user_verification_keys + # verify_login_change_table :user_login_change_keys + # reset_password_table :user_password_reset_keys + # remember_table :user_remember_keys + + # The secret key used for hashing public-facing tokens for various features. + # Defaults to Rails `secret_key_base`, but you can use your own secret key. + # hmac_secret "9180872c271f678dc279aabf6610187b7a11f2d23054ce38ebc66980971e993a25f5613ba255956c9ee7bc4afe724eef16abc411a6e2a8642d53b85531685de1" + + # Use path prefix for all routes. + # prefix "/auth" + + # Specify the controller used for view rendering, CSRF, and callbacks. + rails_controller { RodauthController } + + # Make built-in page titles accessible in your views via an instance variable. + title_instance_variable :@page_title + + # Store account status in an integer column without foreign key constraint. + account_status_column :status + + # Store password hash in a column instead of a separate table. + account_password_hash_column :password_hash + + # Set password when creating account instead of when verifying. + # verify_account_set_password? false + + # Change some default param keys. + login_param "email" + login_confirm_param "email-confirm" + # password_confirm_param "confirm_password" + + # Redirect back to originally requested location after authentication. + # login_return_to_requested_location? true + # two_factor_auth_return_to_requested_location? true # if using MFA + + # Autologin the user after they have reset their password. + # reset_password_autologin? true + + # Delete the account record when the user has closed their account. + # delete_account_on_close? true + + # Redirect to the app from login and registration pages if already logged in. + # already_logged_in { redirect login_redirect } + + # ==> Emails + send_email do |email| + # queue email delivery on the mailer after the transaction commits + db.after_commit { email.deliver_later } + end + + # ==> Flash + # Match flash keys with ones already used in the Rails app. + # flash_notice_key :success # default is :notice + # flash_error_key :error # default is :alert + + # Override default flash messages. + # create_account_notice_flash "Your account has been created. Please verify your account by visiting the confirmation link sent to your email address." + # require_login_error_flash "Login is required for accessing this page" + # login_notice_flash nil + + # ==> Validation + # Override default validation error messages. + # no_matching_login_message "user with this email address doesn't exist" + # already_an_account_with_this_login_message "user with this email address already exists" + # password_too_short_message { "needs to have at least #{password_minimum_length} characters" } + # login_does_not_meet_requirements_message { "invalid email#{", #{login_requirement_message}" if login_requirement_message}" } + + # Passwords shorter than 8 characters are considered weak according to OWASP. + password_minimum_length 8 + # bcrypt has a maximum input length of 72 bytes, truncating any extra bytes. + password_maximum_bytes 72 + + # Custom password complexity requirements (alternative to password_complexity feature). + # password_meets_requirements? do |password| + # super(password) && password_complex_enough?(password) + # end + # auth_class_eval do + # def password_complex_enough?(password) + # return true if password.match?(/\d/) && password.match?(/[^a-zA-Z\d]/) + # set_password_requirement_error_message(:password_simple, "requires one number and one special character") + # false + # end + # end + + # ==> Remember Feature + # Remember all logged in users. + after_login { remember_login } + + # Or only remember users that have ticked a "Remember Me" checkbox on login. + # after_login { remember_login if param_or_nil("remember") } + + # Extend user's remember period when remembered via a cookie + extend_remember_deadline? true + + # ==> Hooks + # Validate custom fields in the create account form. + # before_create_account do + # throw_error_status(422, "name", "must be present") if param("name").empty? + # end + + # Perform additional actions after the account is created. + # after_create_account do + # Profile.create!(account_id: account_id, name: param("name")) + # end + + # Do additional cleanup after the account is closed. + # after_close_account do + # Profile.find_by!(account_id: account_id).destroy + # end + + # ==> Redirects + # Redirect to home page after logout. + logout_redirect "/" + + # Redirect to wherever login redirects to after account verification. + # verify_account_redirect { login_redirect } + + # Redirect to login page after password reset. + # reset_password_redirect { login_path } + + # Ensure requiring login follows login route changes. + require_login_redirect { login_path } + + # ==> Deadlines + # Change default deadlines for some actions. + # verify_account_grace_period 3.days.to_i + # reset_password_deadline_interval Hash[hours: 6] + # verify_login_change_deadline_interval Hash[days: 2] + # remember_deadline_interval Hash[days: 30] + end +end diff --git a/app/models/account.rb b/app/models/account.rb new file mode 100644 index 0000000..ecaffb9 --- /dev/null +++ b/app/models/account.rb @@ -0,0 +1,5 @@ +class Account < ApplicationRecord + include Rodauth::Rails.model + + enum :status, unverified: 1, verified: 2, closed: 3 +end diff --git a/app/views/backoffice/show.html.erb b/app/views/backoffice/show.html.erb new file mode 100644 index 0000000..936bdb1 --- /dev/null +++ b/app/views/backoffice/show.html.erb @@ -0,0 +1,24 @@ +

Einstellungen

+ +

Hier wird es irgendwann mal was einzustellen geben.

+

+ +<%= Checklist.count %> +<%= link_to Checklist.model_name.human(count: Checklist.count), :checklists %> +

+ +

+ +<%= Check.count %> +<%= link_to Check.model_name.human(count: Check.count), :checks %> +

+ +

+ +<%= Link.count %> +<%= link_to Link.model_name.human(count: Link.count), :links %> +

+ +

+ <%= link_to "Backup herunterladen", admin_backup_url, class: "btn btn-secondary", data: { turbo_prefetch: false, frame: "_top", turbo: false } %> +

\ No newline at end of file diff --git a/app/views/home/root.html.erb b/app/views/home/root.html.erb new file mode 100644 index 0000000..f14a0d2 --- /dev/null +++ b/app/views/home/root.html.erb @@ -0,0 +1,2 @@ +

Public#root

+

Find me in app/views/public/root.html.erb

diff --git a/app/views/home/show.html.erb b/app/views/home/show.html.erb index c72ee9d..117bbf5 100644 --- a/app/views/home/show.html.erb +++ b/app/views/home/show.html.erb @@ -7,25 +7,4 @@ <%= Report.count %> <%= link_to Report.model_name.human(count: Report.count), :reports %>

-

- -<%= Checklist.count %> -<%= link_to Checklist.model_name.human(count: Checklist.count), :checklists %> -

-

- -<%= Check.count %> -<%= link_to Check.model_name.human(count: Check.count), :checks %> -

- -

- -<%= Link.count %> -<%= link_to Link.model_name.human(count: Link.count), :links %> -

- - -

- <%= link_to "Backup herunterladen", admin_backup_url, class: "btn btn-secondary", data: { turbo_prefetch: false, frame: "_top", turbo: false } %> -

\ No newline at end of file diff --git a/app/views/layouts/_flash.html.erb b/app/views/layouts/_flash.html.erb index 486a703..d62a65c 100644 --- a/app/views/layouts/_flash.html.erb +++ b/app/views/layouts/_flash.html.erb @@ -1,5 +1,5 @@ <% if flash[:alert] || flash[:notice] %> -
+
<% if flash[:alert] %>