diff --git a/.forgejo/workflows/ci_cd.yml b/.forgejo/workflows/ci_cd.yml index 0beaac1..1a18ac4 100644 --- a/.forgejo/workflows/ci_cd.yml +++ b/.forgejo/workflows/ci_cd.yml @@ -37,6 +37,7 @@ jobs: services: chrome: image: selenium/standalone-chrome + shm_size: 2g steps: - name: Cache repository uses: actions/cache@v4 diff --git a/.vscode/launch.json b/.vscode/launch.json index b391a9e..1a15b0d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,16 +4,11 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - // { - // "type": "rdbg", - // "request": "attach", - // "name": "Attach to a debuggee" - // }, { "type": "ruby_lsp", "name": "Debug server", "request": "attach", - "preLaunchTask": "dev" - }, + "preLaunchTask": "dev", + } ] } \ No newline at end of file diff --git a/Gemfile b/Gemfile index d9a6c72..4819425 100644 --- a/Gemfile +++ b/Gemfile @@ -5,14 +5,13 @@ source "https://rubygems.org" ruby "3.2.5" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem "rails", "~> 7.2" +gem "rails", "~> 8.0" -# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] -gem "sprockets-rails" +gem "propshaft" # Use sqlite3 as the database for Active Record # gem "sqlite3", "~> 1.4" -gem "sqlite3", ">= 2.0" +gem "sqlite3", ">= 2.1" # Use the Puma web server [https://github.com/puma/puma] gem "puma", ">= 5.0" @@ -39,7 +38,7 @@ gem "jbuilder" # gem "kredis" # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] -# gem "bcrypt", "~> 3.1.7" +gem "bcrypt", "~> 3.1.7" # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem "tzinfo-data", platforms: %i[windows jruby] @@ -48,7 +47,6 @@ gem "tzinfo-data", platforms: %i[windows jruby] gem "bootsnap", require: false # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] -gem "activerecord-enhancedsqlite3-adapter" gem "bootstrap_form" gem "caxlsx" gem "caxlsx_rails" @@ -58,7 +56,6 @@ 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 14bb19a..be0da77 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,70 +1,67 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.2.0) - actionpack (= 7.2.0) - activesupport (= 7.2.0) + actioncable (8.0.0) + actionpack (= 8.0.0) + activesupport (= 8.0.0) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.0) - actionpack (= 7.2.0) - activejob (= 7.2.0) - activerecord (= 7.2.0) - activestorage (= 7.2.0) - activesupport (= 7.2.0) + actionmailbox (8.0.0) + actionpack (= 8.0.0) + activejob (= 8.0.0) + activerecord (= 8.0.0) + activestorage (= 8.0.0) + activesupport (= 8.0.0) mail (>= 2.8.0) - actionmailer (7.2.0) - actionpack (= 7.2.0) - actionview (= 7.2.0) - activejob (= 7.2.0) - activesupport (= 7.2.0) + actionmailer (8.0.0) + actionpack (= 8.0.0) + actionview (= 8.0.0) + activejob (= 8.0.0) + activesupport (= 8.0.0) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.0) - actionview (= 7.2.0) - activesupport (= 7.2.0) + actionpack (8.0.0) + actionview (= 8.0.0) + activesupport (= 8.0.0) nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4, < 3.2) + rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (7.2.0) - actionpack (= 7.2.0) - activerecord (= 7.2.0) - activestorage (= 7.2.0) - activesupport (= 7.2.0) + actiontext (8.0.0) + actionpack (= 8.0.0) + activerecord (= 8.0.0) + activestorage (= 8.0.0) + activesupport (= 8.0.0) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.0) - activesupport (= 7.2.0) + actionview (8.0.0) + activesupport (= 8.0.0) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.2.0) - activesupport (= 7.2.0) + activejob (8.0.0) + activesupport (= 8.0.0) globalid (>= 0.3.6) - activemodel (7.2.0) - activesupport (= 7.2.0) - activerecord (7.2.0) - activemodel (= 7.2.0) - activesupport (= 7.2.0) + activemodel (8.0.0) + activesupport (= 8.0.0) + activerecord (8.0.0) + activemodel (= 8.0.0) + activesupport (= 8.0.0) timeout (>= 0.4.0) - activerecord-enhancedsqlite3-adapter (0.8.0) - activerecord (>= 7.1) - sqlite3 (>= 1.6) - activestorage (7.2.0) - actionpack (= 7.2.0) - activejob (= 7.2.0) - activerecord (= 7.2.0) - activesupport (= 7.2.0) + activestorage (8.0.0) + actionpack (= 8.0.0) + activejob (= 8.0.0) + activerecord (= 8.0.0) + activesupport (= 8.0.0) marcel (~> 1.0) - activesupport (7.2.0) + activesupport (8.0.0) base64 + benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) @@ -74,14 +71,13 @@ GEM minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) 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) + benchmark (0.4.0) bigdecimal (3.1.8) bindex (0.8.1) bootsnap (1.18.3) @@ -114,7 +110,7 @@ GEM crass (1.0.6) cssbundling-rails (1.4.0) railties (>= 6.0.0) - date (3.3.4) + date (3.4.0) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) @@ -163,7 +159,7 @@ GEM mini_mime (1.1.5) minitest (5.24.1) msgpack (1.7.2) - net-imap (0.4.14) + net-imap (0.5.0) date net-protocol net-pop (0.1.2) @@ -220,6 +216,11 @@ GEM prawn-table (0.2.2) prawn (>= 1.3.0, < 3.0.0) prism (1.2.0) + propshaft (1.1.0) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + railties (>= 7.0.0) psych (5.1.2) stringio public_suffix (6.0.0) @@ -234,20 +235,20 @@ GEM rackup (2.1.0) rack (>= 3) webrick (~> 1.8) - rails (7.2.0) - actioncable (= 7.2.0) - actionmailbox (= 7.2.0) - actionmailer (= 7.2.0) - actionpack (= 7.2.0) - actiontext (= 7.2.0) - actionview (= 7.2.0) - activejob (= 7.2.0) - activemodel (= 7.2.0) - activerecord (= 7.2.0) - activestorage (= 7.2.0) - activesupport (= 7.2.0) + rails (8.0.0) + actioncable (= 8.0.0) + actionmailbox (= 8.0.0) + actionmailer (= 8.0.0) + actionpack (= 8.0.0) + actiontext (= 8.0.0) + actionview (= 8.0.0) + activejob (= 8.0.0) + activemodel (= 8.0.0) + activerecord (= 8.0.0) + activestorage (= 8.0.0) + activesupport (= 8.0.0) bundler (>= 1.15.0) - railties (= 7.2.0) + railties (= 8.0.0) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -255,9 +256,9 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.2.0) - actionpack (= 7.2.0) - activesupport (= 7.2.0) + railties (8.0.0) + actionpack (= 8.0.0) + activesupport (= 8.0.0) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -274,21 +275,6 @@ 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) @@ -341,29 +327,16 @@ 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) sorbet-runtime (0.5.11625) - sprockets (4.2.1) - concurrent-ruby (~> 1.0) - rack (>= 2.2.4, < 4) - sprockets-rails (3.5.1) - actionpack (>= 6.1) - activesupport (>= 6.1) - sprockets (>= 3.0.0) - sqlite3 (2.0.3-aarch64-linux-gnu) - sqlite3 (2.0.3-arm-linux-gnu) - sqlite3 (2.0.3-arm64-darwin) - sqlite3 (2.0.3-x86-linux-gnu) - sqlite3 (2.0.3-x86_64-darwin) - sqlite3 (2.0.3-x86_64-linux-gnu) + sqlite3 (2.2.0-aarch64-linux-gnu) + sqlite3 (2.2.0-arm-linux-gnu) + sqlite3 (2.2.0-arm64-darwin) + sqlite3 (2.2.0-x86-linux-gnu) + sqlite3 (2.2.0-x86_64-darwin) + sqlite3 (2.2.0-x86_64-linux-gnu) stimulus-rails (1.3.3) railties (>= 6.0.0) stringio (3.1.1) @@ -381,6 +354,7 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) + uri (1.0.1) useragent (0.16.10) web-console (4.2.1) actionview (>= 6.0.0) @@ -405,7 +379,7 @@ PLATFORMS x86_64-linux DEPENDENCIES - activerecord-enhancedsqlite3-adapter + bcrypt (~> 3.1.7) bootsnap bootstrap_form brakeman @@ -423,9 +397,9 @@ DEPENDENCIES pandoc-ruby prawn-markup prawn-rails + propshaft puma (>= 5.0) - rails (~> 7.2) - rodauth-rails + rails (~> 8.0) rubocop rubocop-capybara rubocop-rails @@ -435,8 +409,7 @@ DEPENDENCIES sablon selenium-webdriver slim - sprockets-rails - sqlite3 (>= 2.0) + sqlite3 (>= 2.1) stimulus-rails turbo-rails tzinfo-data diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js deleted file mode 100644 index 9a99757..0000000 --- a/app/assets/config/manifest.js +++ /dev/null @@ -1,2 +0,0 @@ -//= link_tree ../images -//= link_tree ../builds diff --git a/app/assets/stylesheets/application.bootstrap.scss b/app/assets/stylesheets/application.bootstrap.scss index 19b2bb7..aa2568b 100644 --- a/app/assets/stylesheets/application.bootstrap.scss +++ b/app/assets/stylesheets/application.bootstrap.scss @@ -2,10 +2,10 @@ font-family: 'Lexend'; src: url('Lexend-VariableFont_wght.ttf'); font-display: swap; - } +} -$font-family-sans-serif: - Lexend, + +$font-family-sans-serif: Lexend, // Cross-platform generic font family (default user interface font) system-ui, // Safari for macOS and iOS (San Francisco) @@ -29,6 +29,13 @@ $enable-rounded: false; @import 'bootstrap/scss/bootstrap'; @import 'bootstrap-icons/font/bootstrap-icons'; +@font-face { + font-display: block; + font-family: "bootstrap-icons"; + src: url("./bootstrap-icons.woff2") format("woff2"), + url("./bootstrap-icons.woff") format("woff"); +} + .rails-bootstrap-forms-date-select select, .rails-bootstrap-forms-time-select select, .rails-bootstrap-forms-datetime-select select { @@ -56,16 +63,17 @@ $enable-rounded: false; * element we wrap around attachments. Otherwise, * images in galleries will be squished by the max-width: 33%; rule. */ -.trix-content .attachment-gallery > action-text-attachment, -.trix-content .attachment-gallery > .attachment { +.trix-content .attachment-gallery>action-text-attachment, +.trix-content .attachment-gallery>.attachment { flex: 1 0 33%; padding: 0 0.5em; max-width: 33%; } -.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment, -.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment, -.trix-content .attachment-gallery.attachment-gallery--4 > .attachment { +.trix-content .attachment-gallery.attachment-gallery--2>action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--2>.attachment, +.trix-content .attachment-gallery.attachment-gallery--4>action-text-attachment, +.trix-content .attachment-gallery.attachment-gallery--4>.attachment { flex-basis: 50%; max-width: 50%; } @@ -79,25 +87,26 @@ $enable-rounded: false; /* Fix trix dark mode */ .trix-button-row { .trix-button-group { - border: var(--bs-border-width) solid var(--bs-border-color); + border: var(--bs-border-width) solid var(--bs-border-color); - .trix-button { - border: 0; - padding: var(--bs-padding) - } + .trix-button { + border: 0; + padding: var(--bs-padding) + } } } [data-bs-theme=dark] { .trix-button-row { - .trix-button-group { - .trix-button { - background-color: transparent !important; - filter: invert(100%) !important; - } + .trix-button-group { + .trix-button { + background-color: transparent !important; + filter: invert(100%) !important; } + } } } + /* end fix trix dark mode */ .trix-content { @@ -107,9 +116,11 @@ $enable-rounded: false; border: var(--bs-border-width) solid var(--bs-border-color) !important; border-radius: 0 !important; } + p { margin-bottom: 0.6rem; } + p:last-child { margin-bottom: 0; } @@ -121,7 +132,7 @@ trix-toolbar .trix-dialog { color: var(--bs-secondary-color) !important; border: var(--bs-border-width) solid var(--bs-border-color) !important; border-radius: 0 !important; - box-shadow: none; + box-shadow: none; font-size: 1.1rem; } @@ -145,6 +156,7 @@ trix-toolbar .trix-input--dialog { trix-toolbar .trix-dialog--link { max-width: 900px; } + /* trix-editor.trix-content { min-height: 350px; overflow-y: auto; diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 8d6c2a1..4264c74 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,6 +1,16 @@ -# frozen_string_literal: true - module ApplicationCable class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + set_current_user || reject_unauthorized_connection + end + + private + def set_current_user + if session = Session.find_by(id: cookies.signed[:session_id]) + self.current_user = session.user + end + end end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a94b0a0..3804ad6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class ApplicationController < ActionController::Base + include Authentication include Pagy::Backend # allow_browser versions: :modern @@ -13,15 +14,29 @@ class ApplicationController < ActionController::Base def initialize_navbar return unless request.get? - @navbar_items = if rodauth.logged_in? + @navbar_items = if authenticated? [ - { 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 } + { + 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: "Konto", + path: profile_path + } ] else - [ { label: "Login", icon: :'door-closed', path: rodauth.login_path, label_class: "text-info" } ] + [ { label: "Login", icon: :'door-closed', path: new_session_path, label_class: "text-info" } ] end @nav_path = controller_name @search_url = nil diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb new file mode 100644 index 0000000..771b21d --- /dev/null +++ b/app/controllers/concerns/authentication.rb @@ -0,0 +1,55 @@ +module Authentication + extend ActiveSupport::Concern + + included do + before_action :require_authentication + helper_method :authenticated? + end + + class_methods do + def allow_unauthenticated_access(**options) + skip_before_action :require_authentication, **options + end + end + + private + def authenticated? + resume_session + end + + def require_authentication + resume_session || request_authentication + end + + + def resume_session + Current.session ||= find_session_by_cookie + end + + def find_session_by_cookie + Session.find_by(id: cookies.signed[:session_id]) + end + + + def request_authentication + session[:return_to_after_authenticating] = request.url + redirect_to new_session_path + end + + def after_authentication_url + session.delete(:return_to_after_authenticating) || root_url + end + + + def start_new_session_for(user) + user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session| + Current.session = session + cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax } + end + end + + def terminate_session + Current.session.destroy + cookies.delete(:session_id) + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index f8ddff1..9d3293e 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,10 +1,36 @@ # frozen_string_literal: true class HomeController < ApplicationController + allow_unauthenticated_access only: [ :show ] + def show - if rodauth.logged_in? - else - render :root - end + end + + def profile + end + + private + def initialize_sidebar_items + return [] unless action_name == "profile" + + [ + { + label: "Profil", + icon: :person, + path: profile_path, + active: action_name == "profile" + }, + # { + # label: "Passwort ändern", + # path: new_password_path, + # icon: :lock + # }, + { + label: "Logout", + icon: :"box-arrow-right", + path: session_path, + method: :delete + } + ] end end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 0000000..0c4b4a8 --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,33 @@ +class PasswordsController < ApplicationController + allow_unauthenticated_access + before_action :set_user_by_token, only: %i[ edit update ] + + def new + end + + def create + if user = User.find_by(email_address: params[:email_address]) + PasswordsMailer.reset(user).deliver_later + end + + redirect_to new_session_path, notice: "Password reset instructions sent (if user with that email address exists)." + end + + def edit + end + + def update + if @user.update(params.permit(:password, :password_confirmation)) + redirect_to new_session_path, notice: "Password has been reset." + else + redirect_to edit_password_path(params[:token]), alert: "Passwords did not match." + end + end + + private + def set_user_by_token + @user = User.find_by_password_reset_token!(params[:token]) + rescue ActiveSupport::MessageVerifier::InvalidSignature + redirect_to new_password_path, alert: "Password reset link is invalid or has expired." + end +end diff --git a/app/controllers/rodauth_controller.rb b/app/controllers/rodauth_controller.rb deleted file mode 100644 index 9703c74..0000000 --- a/app/controllers/rodauth_controller.rb +++ /dev/null @@ -1,39 +0,0 @@ -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 - return unless rodauth.logged_in? - - [ - { 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/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..9f4e0ff --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,24 @@ +class SessionsController < ApplicationController + allow_unauthenticated_access only: %i[ new create ] + rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." } + + def new + redirect_to :root if authenticated? + end + + def create + redirect_to :root if authenticated? + + if user = User.authenticate_by(params.permit(:email_address, :password)) + start_new_session_for user + redirect_to after_authentication_url + else + redirect_to new_session_path, alert: "Try another email address or password." + end + end + + def destroy + terminate_session + redirect_to new_session_path + end +end diff --git a/app/mailers/passwords_mailer.rb b/app/mailers/passwords_mailer.rb new file mode 100644 index 0000000..4f0ac7f --- /dev/null +++ b/app/mailers/passwords_mailer.rb @@ -0,0 +1,6 @@ +class PasswordsMailer < ApplicationMailer + def reset(user) + @user = user + mail subject: "Reset your password", to: user.email_address + end +end diff --git a/app/misc/rodauth_app.rb b/app/misc/rodauth_app.rb deleted file mode 100644 index 6372422..0000000 --- a/app/misc/rodauth_app.rb +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index fcfdc17..0000000 --- a/app/misc/rodauth_main.rb +++ /dev/null @@ -1,154 +0,0 @@ -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 deleted file mode 100644 index ecaffb9..0000000 --- a/app/models/account.rb +++ /dev/null @@ -1,5 +0,0 @@ -class Account < ApplicationRecord - include Rodauth::Rails.model - - enum :status, unverified: 1, verified: 2, closed: 3 -end diff --git a/app/models/current.rb b/app/models/current.rb new file mode 100644 index 0000000..2bef56d --- /dev/null +++ b/app/models/current.rb @@ -0,0 +1,4 @@ +class Current < ActiveSupport::CurrentAttributes + attribute :session + delegate :user, to: :session, allow_nil: true +end diff --git a/app/models/session.rb b/app/models/session.rb new file mode 100644 index 0000000..cf376fb --- /dev/null +++ b/app/models/session.rb @@ -0,0 +1,3 @@ +class Session < ApplicationRecord + belongs_to :user +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..c88d5b0 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,6 @@ +class User < ApplicationRecord + has_secure_password + has_many :sessions, dependent: :destroy + + normalizes :email_address, with: ->(e) { e.strip.downcase } +end diff --git a/app/views/home/profile.html.slim b/app/views/home/profile.html.slim new file mode 100644 index 0000000..5169550 --- /dev/null +++ b/app/views/home/profile.html.slim @@ -0,0 +1,2 @@ +pre + == JSON.pretty_generate(Current.session.attributes.merge(user: Current.session.user.attributes)) \ No newline at end of file diff --git a/app/views/layouts/_sidebar.html.erb b/app/views/layouts/_sidebar.html.erb index 894c1dc..95786e4 100644 --- a/app/views/layouts/_sidebar.html.erb +++ b/app/views/layouts/_sidebar.html.erb @@ -4,7 +4,7 @@ <% if @sidebar_items %> <% @sidebar_items.each do |sidebar_item| %> <% if sidebar_item[:method].present? && sidebar_item[:method] != :get %> - <%= button_to(sidebar_item[:path], method: sidebar_item[:method], class: "list-group-item border-end-0 d-inline-block text-truncate") do %> + <%= button_to(sidebar_item[:path], method: sidebar_item[:method], class: "list-group-item border-0 d-inline-block text-truncate") do %> <% if sidebar_item[:icon] %> <% end %> diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb new file mode 100644 index 0000000..9f0c87c --- /dev/null +++ b/app/views/passwords/edit.html.erb @@ -0,0 +1,9 @@ +

Update your password

+ +<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> + +<%= form_with url: password_path(params[:token]), method: :put do |form| %> + <%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72 %>
+ <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72 %>
+ <%= form.submit "Save" %> +<% end %> diff --git a/app/views/passwords/new.html.erb b/app/views/passwords/new.html.erb new file mode 100644 index 0000000..44efb2b --- /dev/null +++ b/app/views/passwords/new.html.erb @@ -0,0 +1,8 @@ +

Forgot your password?

+ +<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %> + +<%= form_with url: passwords_path do |form| %> + <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %>
+ <%= form.submit "Email reset instructions" %> +<% end %> diff --git a/app/views/passwords_mailer/reset.html.erb b/app/views/passwords_mailer/reset.html.erb new file mode 100644 index 0000000..4a06619 --- /dev/null +++ b/app/views/passwords_mailer/reset.html.erb @@ -0,0 +1,4 @@ +

+ You can reset your password within the next 15 minutes on + <%= link_to "this password reset page", edit_password_url(@user.password_reset_token) %>. +

diff --git a/app/views/passwords_mailer/reset.text.erb b/app/views/passwords_mailer/reset.text.erb new file mode 100644 index 0000000..2cf03fc --- /dev/null +++ b/app/views/passwords_mailer/reset.text.erb @@ -0,0 +1,2 @@ +You can reset your password within the next 15 minutes on this password reset page: +<%= edit_password_url(@user.password_reset_token) %> diff --git a/app/views/rodauth/_login_form.html.erb b/app/views/rodauth/_login_form.html.erb deleted file mode 100644 index ea6a42f..0000000 --- a/app/views/rodauth/_login_form.html.erb +++ /dev/null @@ -1,26 +0,0 @@ -<%= form_with url: rodauth.login_path, method: :post, data: { turbo: false } do |form| %> - <% if rodauth.skip_login_field_on_login? %> -
- <%= form.label "login", rodauth.login_label, class: "form-label" %> - <%= form.email_field rodauth.login_param, value: params[rodauth.login_param], id: "login", readonly: true, class: "form-control-plaintext" %> -
- <% else %> -
- <%= form.label "login", rodauth.login_label, class: "form-label" %> - <%= form.email_field rodauth.login_param, value: params[rodauth.login_param], id: "login", autocomplete: rodauth.login_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.login_param)}", aria: ({ invalid: true, describedby: "login_error_message" } if rodauth.field_error(rodauth.login_param)) %> - <%= content_tag(:span, rodauth.field_error(rodauth.login_param), class: "invalid-feedback", id: "login_error_message") if rodauth.field_error(rodauth.login_param) %> -
- <% end %> - - <% unless rodauth.skip_password_field_on_login? %> -
- <%= form.label "password", rodauth.password_label, class: "form-label" %> - <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> - <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> -
- <% end %> - -
- <%= form.submit rodauth.login_button, class: "btn btn-primary" %> -
-<% end %> diff --git a/app/views/rodauth/_login_form_footer.html.erb b/app/views/rodauth/_login_form_footer.html.erb deleted file mode 100644 index 4606ef0..0000000 --- a/app/views/rodauth/_login_form_footer.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<% unless rodauth.login_form_footer_links.empty? %> - <%== rodauth.login_form_footer_links_heading %> - - -<% end %> diff --git a/app/views/rodauth/change_password.html.erb b/app/views/rodauth/change_password.html.erb deleted file mode 100644 index aebeb16..0000000 --- a/app/views/rodauth/change_password.html.erb +++ /dev/null @@ -1,32 +0,0 @@ -

Passwort ändern

-
-
- <%= form_with url: rodauth.change_password_path, method: :post, data: { turbo: false } do |form| %> - <% if rodauth.change_password_requires_password? %> -
- <%= form.label "password", rodauth.password_label, class: "form-label" %> - <%= form.password_field rodauth.password_param, value: "", id: "password", autocomplete: rodauth.password_field_autocomplete_value, required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_param)}", aria: ({ invalid: true, describedby: "password_error_message" } if rodauth.field_error(rodauth.password_param)) %> - <%= content_tag(:span, rodauth.field_error(rodauth.password_param), class: "invalid-feedback", id: "password_error_message") if rodauth.field_error(rodauth.password_param) %> -
- <% end %> - -
- <%= form.label "new-password", rodauth.new_password_label, class: "form-label" %> - <%= form.password_field rodauth.new_password_param, value: "", id: "new-password", autocomplete: "new-password", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.new_password_param)}", aria: ({ invalid: true, describedby: "new-password_error_message" } if rodauth.field_error(rodauth.new_password_param)) %> - <%= content_tag(:span, rodauth.field_error(rodauth.new_password_param), class: "invalid-feedback", id: "new-password_error_message") if rodauth.field_error(rodauth.new_password_param) %> -
- - <% if rodauth.require_password_confirmation? %> -
- <%= form.label "password-confirm", rodauth.password_confirm_label, class: "form-label" %> - <%= form.password_field rodauth.password_confirm_param, value: "", id: "password-confirm", autocomplete: "new-password", required: true, class: "form-control #{"is-invalid" if rodauth.field_error(rodauth.password_confirm_param)}", aria: ({ invalid: true, describedby: "password-confirm_error_message" } if rodauth.field_error(rodauth.password_confirm_param)) %> - <%= content_tag(:span, rodauth.field_error(rodauth.password_confirm_param), class: "invalid-feedback", id: "password-confirm_error_message") if rodauth.field_error(rodauth.password_confirm_param) %> -
- <% end %> - -
- <%= form.submit rodauth.change_password_button, class: "btn btn-primary" %> -
- <% end %> -
-
\ No newline at end of file diff --git a/app/views/rodauth/login.html.erb b/app/views/rodauth/login.html.erb deleted file mode 100644 index 636020e..0000000 --- a/app/views/rodauth/login.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<%== rodauth.login_form_header %> -
-
- <%= render "login_form" %> -
-
-<%== rodauth.login_form_footer %> diff --git a/app/views/rodauth/logout.html.erb b/app/views/rodauth/logout.html.erb deleted file mode 100644 index 1d7df86..0000000 --- a/app/views/rodauth/logout.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -

Adieu

-<%= form_with url: rodauth.logout_path, method: :post, data: { turbo: false } do |form| %> - <% if rodauth.features.include?(:active_sessions) %> -
-
- <%= form.check_box rodauth.global_logout_param, id: "global-logout", class: "form-check-input", include_hidden: false %> - <%= form.label "global-logout", rodauth.global_logout_label, class: "form-check-label" %> -
-
- <% end %> -
- <%= form.submit "Log out", class: "btn btn-warning" %> -
-<% end %> diff --git a/app/views/rodauth/multi_phase_login.html.erb b/app/views/rodauth/multi_phase_login.html.erb deleted file mode 100644 index f7da8de..0000000 --- a/app/views/rodauth/multi_phase_login.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%== rodauth.login_form_header %> -<%== rodauth.render_multi_phase_login_forms %> -<%== rodauth.login_form_footer %> diff --git a/app/views/rodauth/profile.html.erb b/app/views/rodauth/profile.html.erb deleted file mode 100644 index a66b35b..0000000 --- a/app/views/rodauth/profile.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -

<%= current_account.email %>

- -
<%= JSON.pretty_generate current_account.attributes %>
\ No newline at end of file diff --git a/app/views/rodauth/remember.html.erb b/app/views/rodauth/remember.html.erb deleted file mode 100644 index 4c0286b..0000000 --- a/app/views/rodauth/remember.html.erb +++ /dev/null @@ -1,22 +0,0 @@ -<%= form_with url: rodauth.remember_path, method: :post, data: { turbo: false } do |form| %> -
-
- <%= form.radio_button rodauth.remember_param, rodauth.remember_remember_param_value, id: "remember-remember", class: "form-check-input" %> - <%= form.label "remember-remember", rodauth.remember_remember_label, class: "form-check-label" %> -
- -
- <%= form.radio_button rodauth.remember_param, rodauth.remember_forget_param_value, id: "remember-forget", class: "form-check-input" %> - <%= form.label "remember-forget", rodauth.remember_forget_label, class: "form-check-label" %> -
- -
- <%= form.radio_button rodauth.remember_param, rodauth.remember_disable_param_value, id: "remember-disable", class: "form-check-input" %> - <%= form.label "remember-disable", rodauth.remember_disable_label, class: "form-check-label" %> -
-
- -
- <%= form.submit rodauth.remember_button, class: "btn btn-primary" %> -
-<% end %> diff --git a/app/views/sessions/new.html.slim b/app/views/sessions/new.html.slim new file mode 100644 index 0000000..4527375 --- /dev/null +++ b/app/views/sessions/new.html.slim @@ -0,0 +1,13 @@ += tag.div(flash[:alert], style: "color:red") if flash[:alert] += tag.div(flash[:notice], style: "color:green") if flash[:notice] + +h1 Login + += bootstrap_form_with url: session_path do |form| + = form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], label: User.human_attribute_name(:email_address) + br + = form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72, label: User.human_attribute_name(:password) + br + = form.submit "Login" +br += link_to "Forgot password?", new_password_path diff --git a/bin/dev.new b/bin/dev.new new file mode 100755 index 0000000..63284e3 --- /dev/null +++ b/bin/dev.new @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV \ No newline at end of file diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index f8042f4..422f526 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -5,7 +5,7 @@ Rails.application.config.assets.version = '1.0' # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path -Rails.application.config.assets.paths << Rails.root.join('node_modules/bootstrap-icons/font') +# Rails.application.config.assets.paths << Rails.root.join('node_modules/bootstrap-icons/font') # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets diff --git a/config/initializers/rodauth.rb b/config/initializers/rodauth.rb deleted file mode 100644 index a832109..0000000 --- a/config/initializers/rodauth.rb +++ /dev/null @@ -1,3 +0,0 @@ -Rodauth::Rails.configure do |config| - config.app = "RodauthApp" -end diff --git a/config/locales/activerecord.yml b/config/locales/activerecord.yml index d2e6dae..f1692e4 100644 --- a/config/locales/activerecord.yml +++ b/config/locales/activerecord.yml @@ -81,6 +81,9 @@ de-CH: path: Name url: Url notes: Notizen + user: + email_address: Login + password: Passwort models: account: one: Konto diff --git a/config/routes.rb b/config/routes.rb index 5413b0c..82d536f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,36 +1,35 @@ # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html Rails.application.routes.draw do - constraints Rodauth::Rails.authenticate do - namespace :admin do - get "backup", to: "backups#show", as: :backup - end - - resources :checklist_entries - resources :checklists - resources :checks - resources :link_categories - resources :links - resources :reports, shallow: true, except: %i[show] do - resources :pages do - resources :elements do - resources :success_criteria do - collection do - get "from_checklist", action: :new_from_checklist, as: :new_from_checklist - post "from_checklist", action: :create_from_checklist, as: :create_from_checklist - end + resource :session + resources :passwords, param: :token + namespace :admin do + get "backup", to: "backups#show", as: :backup + end + + resources :checklist_entries + resources :checklists + resources :checks + resources :link_categories + resources :links + resources :reports, shallow: true, except: %i[show] do + resources :pages do + resources :elements do + resources :success_criteria do + collection do + get "from_checklist", action: :new_from_checklist, as: :new_from_checklist + post "from_checklist", action: :create_from_checklist, as: :create_from_checklist end end end - member do - get "(-/:page_id)", action: :show, as: "", constraints: { id: /\d+/ } - end end - - get "backoffice/show", as: :backoffice - - get "profile", to: "rodauth#profile" + member do + get "(-/:page_id)", action: :show, as: "", constraints: { id: /\d+/ } + end end + get "backoffice/show", as: :backoffice + get "profile" => "home#profile", as: :profile + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get 'up' => 'rails/health#show', as: :rails_health_check diff --git a/db/migrate/20241108175901_create_users.rb b/db/migrate/20241108175901_create_users.rb new file mode 100644 index 0000000..2075edf --- /dev/null +++ b/db/migrate/20241108175901_create_users.rb @@ -0,0 +1,11 @@ +class CreateUsers < ActiveRecord::Migration[8.0] + def change + create_table :users do |t| + t.string :email_address, null: false + t.string :password_digest, null: false + + t.timestamps + end + add_index :users, :email_address, unique: true + end +end diff --git a/db/migrate/20241108175905_create_sessions.rb b/db/migrate/20241108175905_create_sessions.rb new file mode 100644 index 0000000..8102f13 --- /dev/null +++ b/db/migrate/20241108175905_create_sessions.rb @@ -0,0 +1,11 @@ +class CreateSessions < ActiveRecord::Migration[8.0] + def change + create_table :sessions do |t| + t.references :user, null: false, foreign_key: true + t.string :ip_address + t.string :user_agent + + t.timestamps + end + end +end diff --git a/db/migrate/20241108180654_drop_accounts.rb b/db/migrate/20241108180654_drop_accounts.rb new file mode 100644 index 0000000..cb029f2 --- /dev/null +++ b/db/migrate/20241108180654_drop_accounts.rb @@ -0,0 +1,17 @@ +class DropAccounts < ActiveRecord::Migration[8.0] + def change + # Used by the remember me feature + drop_table :account_remember_keys, id: false do |t| + t.integer :id, primary_key: true + t.foreign_key :accounts, column: :id + t.string :key, null: false + t.datetime :deadline, null: false + end + drop_table :accounts do |t| + t.integer :status, null: false, default: 1 + t.string :email, null: false + t.index :email, unique: true, where: "status IN (1, 2)" + t.string :password_hash + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e686302..c5b61eb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,19 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_11_01_125547) do - create_table "account_remember_keys", force: :cascade do |t| - t.string "key", null: false - t.datetime "deadline", null: false - end - - create_table "accounts", force: :cascade do |t| - t.integer "status", default: 1, null: false - t.string "email", null: false - t.string "password_hash" - t.index ["email"], name: "index_accounts_on_email", unique: true, where: "status IN (1, 2)" - end - +ActiveRecord::Schema[8.0].define(version: 2024_11_08_180654) do create_table "action_text_rich_texts", force: :cascade do |t| t.string "name", null: false t.text "body" @@ -174,6 +162,15 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_01_125547) do t.datetime "updated_at", null: false end + create_table "sessions", force: :cascade do |t| + t.integer "user_id", null: false + t.string "ip_address" + t.string "user_agent" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_sessions_on_user_id" + end + create_table "standards", force: :cascade do |t| t.string "name_de" t.string "name_en" @@ -199,7 +196,14 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_01_125547) do t.index ["element_id"], name: "index_success_criteria_on_element_id" end - add_foreign_key "account_remember_keys", "accounts", column: "id" + create_table "users", force: :cascade do |t| + t.string "email_address", null: false + t.string "password_digest", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email_address"], name: "index_users_on_email_address", unique: true + end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "checklist_entries", "checklists" @@ -208,6 +212,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_11_01_125547) do add_foreign_key "elements", "pages" add_foreign_key "links", "link_categories" add_foreign_key "pages", "reports" + add_foreign_key "sessions", "users" add_foreign_key "success_criteria", "checks" add_foreign_key "success_criteria", "elements" end diff --git a/db/seeds.rb b/db/seeds.rb index 5b3be50..b0ddeb2 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -91,5 +91,5 @@ Link.create!(url: "https://www.a11yproject.com/", description: "The A11Y Project is a community-driven effort to make digital accessibility easier.", link_category: LinkCategory.find_by(name: "Artikel")) -Account.find_or_initialize_by(email: "admin@example.com").update!(password: "password") -Account.find_or_initialize_by(email: "goran@quiet.ch").update!(password: "password") \ No newline at end of file +User.find_or_initialize_by(email_address: "admin@example.com").update!(password: "password") +User.find_or_initialize_by(email_address: "goran@quiet.ch").update!(password: "password") \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 54e59ce..dc06931 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,3 +39,12 @@ services: chrome: image: selenium/standalone-chrome + shm_size: 2g + labels: + - traefik.http.routers.chrome-${COMPOSE_PROJECT_NAME}.entrypoints=http + - traefik.http.routers.chrome-${COMPOSE_PROJECT_NAME}.rule=Host(`chrome.${COMPOSE_PROJECT_NAME}.localhost`) + - traefik.http.services.chrome-${COMPOSE_PROJECT_NAME}.loadbalancer.server.port=4444 + - traefik.docker.network=traefik + networks: + - traefik + - default diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index 7988bd9..ecd9a67 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -17,19 +17,23 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase end def login(email, password) - visit "/login" + visit "/session/new" + assert_selector("h1", text: "Login") fill_in "Login", with: email - fill_in "Password", with: password + fill_in "Passwort", with: password click_button "Login" + assert_text("Dashboard") + save_screenshot "after_login.png" end def logout - visit "/logout" - click_on "Log out" + Session.destroy_all end def login_test - Account.create(email: "test@example.com", password: "password") - login("test@example.com", "password") + user = User.find_or_initialize_by(email_address: "test@example.com") + user.update!(password: "password") + login(user.email_address, user.password) + save_screenshot "login.png" end end diff --git a/test/controller_test.rb b/test/controller_test.rb new file mode 100644 index 0000000..113245e --- /dev/null +++ b/test/controller_test.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "test_helper" + +class ControllerTest < ActionDispatch::IntegrationTest + def login(email, password) + post "/session", params: { email_address: email, password: password } + assert_redirected_to "/" + end + + def logout + delete "/session" + assert_redirected_to "/session/new" + end +end diff --git a/test/controllers/admin/backups_controller_test.rb b/test/controllers/admin/backups_controller_test.rb index 2db4ad8..1b7c937 100644 --- a/test/controllers/admin/backups_controller_test.rb +++ b/test/controllers/admin/backups_controller_test.rb @@ -3,23 +3,13 @@ require "test_helper" module Admin - class BackupsControllerTest < ActionDispatch::IntegrationTest - def login(email, password) - post "/login", params: { email: email, password: password } - assert_redirected_to "/" - end - - def logout - post "/logout" - assert_redirected_to "/" - end - + class BackupsControllerTest < ::ControllerTest teardown do logout end setup do - Account.create(email: "test@example.com", password: "password") + User.create!(email_address: "test@example.com", password: "password") login("test@example.com", "password") end diff --git a/test/controllers/backoffice_controller_test.rb b/test/controllers/backoffice_controller_test.rb index 8e1f237..585f632 100644 --- a/test/controllers/backoffice_controller_test.rb +++ b/test/controllers/backoffice_controller_test.rb @@ -1,22 +1,12 @@ require "test_helper" -class BackofficeControllerTest < ActionDispatch::IntegrationTest - def login(email, password) - post "/login", params: { email: email, password: password } - assert_redirected_to "/" - end - - def logout - post "/logout" - assert_redirected_to "/" - end - +class BackofficeControllerTest < ::ControllerTest teardown do logout end setup do - Account.create(email: "test@example.com", password: "password") + User.create!(email_address: "test@example.com", password: "password") login("test@example.com", "password") end diff --git a/test/controllers/checklist_entries_controller_test.rb b/test/controllers/checklist_entries_controller_test.rb index adba726..6cbe007 100644 --- a/test/controllers/checklist_entries_controller_test.rb +++ b/test/controllers/checklist_entries_controller_test.rb @@ -2,24 +2,14 @@ require "test_helper" -class ChecklistEntriesControllerTest < ActionDispatch::IntegrationTest - def login(email, password) - post "/login", params: { email: email, password: password } - assert_redirected_to "/" - end - - def logout - post "/logout" - assert_redirected_to "/" - end - +class ChecklistEntriesControllerTest < ::ControllerTest teardown do logout end setup do @checklist_entry = checklist_entries(:one) - Account.create(email: "test@example.com", password: "password") + User.create!(email_address: "test@example.com", password: "password") login("test@example.com", "password") end diff --git a/test/controllers/checklists_controller_test.rb b/test/controllers/checklists_controller_test.rb index 83d8ad7..faa30c1 100644 --- a/test/controllers/checklists_controller_test.rb +++ b/test/controllers/checklists_controller_test.rb @@ -2,24 +2,14 @@ require "test_helper" -class ChecklistsControllerTest < ActionDispatch::IntegrationTest - def login(email, password) - post "/login", params: { email: email, password: password } - assert_redirected_to "/" - end - - def logout - post "/logout" - assert_redirected_to "/" - end - +class ChecklistsControllerTest < ::ControllerTest teardown do logout end setup do @checklist = checklists(:one) - Account.create(email: "test@example.com", password: "password") + User.create!(email_address: "test@example.com", password: "password") login("test@example.com", "password") end diff --git a/test/controllers/checks_controller_test.rb b/test/controllers/checks_controller_test.rb index ebcd0c9..f66c842 100644 --- a/test/controllers/checks_controller_test.rb +++ b/test/controllers/checks_controller_test.rb @@ -2,17 +2,7 @@ require "test_helper" -class ChecksControllerTest < ActionDispatch::IntegrationTest - def login(email, password) - post "/login", params: { email: email, password: password } - assert_redirected_to "/" - end - - def logout - post "/logout" - assert_redirected_to "/" - end - +class ChecksControllerTest < ::ControllerTest teardown do logout end @@ -20,7 +10,7 @@ class ChecksControllerTest < ActionDispatch::IntegrationTest setup do @principle = principles(:one) @check = checks(:deletable) - Account.create(email: "test@example.com", password: "password") + User.create!(email_address: "test@example.com", password: "password") login("test@example.com", "password") end diff --git a/test/controllers/elements_controller_test.rb b/test/controllers/elements_controller_test.rb index 3651294..7a58f68 100644 --- a/test/controllers/elements_controller_test.rb +++ b/test/controllers/elements_controller_test.rb @@ -2,17 +2,7 @@ require "test_helper" -class ElementsControllerTest < ActionDispatch::IntegrationTest - def login(email, password) - post "/login", params: { email: email, password: password } - assert_redirected_to "/" - end - - def logout - post "/logout" - assert_redirected_to "/" - end - +class ElementsControllerTest < ::ControllerTest teardown do logout end @@ -20,7 +10,7 @@ class ElementsControllerTest < ActionDispatch::IntegrationTest setup do @element = elements(:one) @checklist = checklists(:one) - Account.create(email: "test@example.com", password: "password") + User.create!(email_address: "test@example.com", password: "password") login("test@example.com", "password") end diff --git a/test/controllers/home_controller_test.rb b/test/controllers/home_controller_test.rb index 28297fc..9abdddd 100644 --- a/test/controllers/home_controller_test.rb +++ b/test/controllers/home_controller_test.rb @@ -2,7 +2,7 @@ require "test_helper" -class HomeControllerTest < ActionDispatch::IntegrationTest +class HomeControllerTest < ::ControllerTest test "should get show" do get root_url assert_response :success diff --git a/test/controllers/link_categories_controller_test.rb b/test/controllers/link_categories_controller_test.rb index 144ee98..9877cc4 100644 --- a/test/controllers/link_categories_controller_test.rb +++ b/test/controllers/link_categories_controller_test.rb @@ -2,24 +2,14 @@ require "test_helper" -class LinkCategoriesControllerTest < ActionDispatch::IntegrationTest - def login(email, password) - post "/login", params: { email: email, password: password } - assert_redirected_to "/" - end - - def logout - post "/logout" - assert_redirected_to "/" - end - +class LinkCategoriesControllerTest < ::ControllerTest teardown do logout end setup do @link_category = link_categories(:one) - Account.create(email: "test@example.com", password: "password") + User.create!(email_address: "test@example.com", password: "password") login("test@example.com", "password") end diff --git a/test/controllers/links_controller_test.rb b/test/controllers/links_controller_test.rb index 7e7bdc1..e3a3ad0 100644 --- a/test/controllers/links_controller_test.rb +++ b/test/controllers/links_controller_test.rb @@ -2,24 +2,14 @@ require "test_helper" -class LinksControllerTest < ActionDispatch::IntegrationTest - def login(email, password) - post "/login", params: { email: email, password: password } - assert_redirected_to "/" - end - - def logout - post "/logout" - assert_redirected_to "/" - end - +class LinksControllerTest < ::ControllerTest teardown do logout end setup do @link = links(:one) - Account.create(email: "test@example.com", password: "password") + User.create!(email_address: "test@example.com", password: "password") login("test@example.com", "password") end diff --git a/test/controllers/pages_controller_test.rb b/test/controllers/pages_controller_test.rb index d3ff8b3..432077c 100644 --- a/test/controllers/pages_controller_test.rb +++ b/test/controllers/pages_controller_test.rb @@ -1,22 +1,12 @@ require "test_helper" -class PagesControllerTest < ActionDispatch::IntegrationTest - def login(email, password) - post "/login", params: { email: email, password: password } - assert_redirected_to "/" - end - - def logout - post "/logout" - assert_redirected_to "/" - end - +class PagesControllerTest < ::ControllerTest teardown do logout end setup do - Account.create(email: "test@example.com", password: "password") + User.create!(email_address: "test@example.com", password: "password") @page = pages(:one) login("test@example.com", "password") end diff --git a/test/controllers/reports_controller_test.rb b/test/controllers/reports_controller_test.rb index 23d3979..f46759f 100644 --- a/test/controllers/reports_controller_test.rb +++ b/test/controllers/reports_controller_test.rb @@ -2,24 +2,14 @@ require "test_helper" -class ReportsControllerTest < ActionDispatch::IntegrationTest - def login(email, password) - post "/login", params: { email: email, password: password } - assert_redirected_to "/" - end - - def logout - post "/logout" - assert_redirected_to "/" - end - +class ReportsControllerTest < ::ControllerTest teardown do logout end setup do @report = reports(:one) - Account.create(email: "test@example.com", password: "password") + User.create!(email_address: "test@example.com", password: "password") login("test@example.com", "password") end diff --git a/test/controllers/success_criteria_controller_test.rb b/test/controllers/success_criteria_controller_test.rb index b1d801d..903337d 100644 --- a/test/controllers/success_criteria_controller_test.rb +++ b/test/controllers/success_criteria_controller_test.rb @@ -2,24 +2,14 @@ require "test_helper" -class SuccessCriteriaControllerTest < ActionDispatch::IntegrationTest - def login(email, password) - post "/login", params: { email: email, password: password } - assert_redirected_to "/" - end - - def logout - post "/logout" - assert_redirected_to "/" - end - +class SuccessCriteriaControllerTest < ::ControllerTest teardown do logout end setup do @success_criterion = success_criteria(:one) - Account.create(email: "test@example.com", password: "password") + User.create!(email_address: "test@example.com", password: "password") login("test@example.com", "password") end diff --git a/test/fixtures/accounts.yml b/test/fixtures/accounts.yml deleted file mode 100644 index 1a33bd6..0000000 --- a/test/fixtures/accounts.yml +++ /dev/null @@ -1,10 +0,0 @@ -# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html -one: - email: freddie@queen.com - password_hash: <%= RodauthApp.rodauth.allocate.password_hash("password") %> - status: verified - -two: - email: brian@queen.com - password_hash: <%= RodauthApp.rodauth.allocate.password_hash("password") %> - status: verified diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 0000000..0951563 --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,9 @@ +<% password_digest = BCrypt::Password.create("password") %> + +one: + email_address: one@example.com + password_digest: <%= password_digest %> + +two: + email_address: two@example.com + password_digest: <%= password_digest %> diff --git a/test/mailers/previews/passwords_mailer_preview.rb b/test/mailers/previews/passwords_mailer_preview.rb new file mode 100644 index 0000000..01d07ec --- /dev/null +++ b/test/mailers/previews/passwords_mailer_preview.rb @@ -0,0 +1,7 @@ +# Preview all emails at http://localhost:3000/rails/mailers/passwords_mailer +class PasswordsMailerPreview < ActionMailer::Preview + # Preview this email at http://localhost:3000/rails/mailers/passwords_mailer/reset + def reset + PasswordsMailer.reset(User.take) + end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 0000000..5c07f49 --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class UserTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index a84c711..7ed015f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -4,12 +4,14 @@ ENV["RAILS_ENV"] ||= "test" require_relative "../config/environment" require "rails/test_help" require "action_text/system_test_helper" +require "capybara/rails" +require "controller_test" module ActiveSupport class TestCase include ActionText::SystemTestHelper # Run tests in parallel with specified workers - parallelize(workers: :number_of_processors) + # parallelize(workers: :number_of_processors) # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all diff --git a/vendor/assets/fonts/bootstrap-icons.woff b/vendor/assets/fonts/bootstrap-icons.woff new file mode 100644 index 0000000..51204d2 Binary files /dev/null and b/vendor/assets/fonts/bootstrap-icons.woff differ diff --git a/vendor/assets/fonts/bootstrap-icons.woff2 b/vendor/assets/fonts/bootstrap-icons.woff2 new file mode 100644 index 0000000..92c4830 Binary files /dev/null and b/vendor/assets/fonts/bootstrap-icons.woff2 differ