Add auth and ruby update

This commit is contained in:
david 2024-09-22 21:57:05 +02:00
parent 5d50194f39
commit fbf6923835
43 changed files with 614 additions and 64 deletions

View file

@ -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

View file

@ -0,0 +1,6 @@
class BackofficeController < ApplicationController
include BackofficeMenu
def show
end
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -2,5 +2,9 @@
class HomeController < ApplicationController
def show
if rodauth.logged_in?
else
render :root
end
end
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,2 @@
module BackofficeHelper
end

View file

@ -0,0 +1,2 @@
module PublicHelper
end

25
app/misc/rodauth_app.rb Normal file
View file

@ -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

154
app/misc/rodauth_main.rb Normal file
View file

@ -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

5
app/models/account.rb Normal file
View file

@ -0,0 +1,5 @@
class Account < ApplicationRecord
include Rodauth::Rails.model
enum :status, unverified: 1, verified: 2, closed: 3
end

View file

@ -0,0 +1,24 @@
<h1>Einstellungen</h1>
<p>Hier wird es irgendwann mal was einzustellen geben.</p>
<p>
<i class="bi bi-list-check"></i>
<%= Checklist.count %>
<%= link_to Checklist.model_name.human(count: Checklist.count), :checklists %>
</p>
<p>
<i class="bi bi-check2"></i>
<%= Check.count %>
<%= link_to Check.model_name.human(count: Check.count), :checks %>
</p>
<p>
<i class="bi bi-link"></i>
<%= Link.count %>
<%= link_to Link.model_name.human(count: Link.count), :links %>
</p>
<p>
<%= link_to "Backup herunterladen", admin_backup_url, class: "btn btn-secondary", data: { turbo_prefetch: false, frame: "_top", turbo: false } %>
</p>

View file

@ -0,0 +1,2 @@
<h1>Public#root</h1>
<p>Find me in app/views/public/root.html.erb</p>

View file

@ -7,25 +7,4 @@
<%= Report.count %>
<%= link_to Report.model_name.human(count: Report.count), :reports %>
</p>
<p>
<i class="bi bi-list-check"></i>
<%= Checklist.count %>
<%= link_to Checklist.model_name.human(count: Checklist.count), :checklists %>
</p>
<p>
<i class="bi bi-check2"></i>
<%= Check.count %>
<%= link_to Check.model_name.human(count: Check.count), :checks %>
</p>
<p>
<i class="bi bi-link"></i>
<%= Link.count %>
<%= link_to Link.model_name.human(count: Link.count), :links %>
</p>
<p>
<%= link_to "Backup herunterladen", admin_backup_url, class: "btn btn-secondary", data: { turbo_prefetch: false, frame: "_top", turbo: false } %>
</p>

View file

@ -1,5 +1,5 @@
<% if flash[:alert] || flash[:notice] %>
<div class="container mt-3 mb-3">
<div class="container-fluid mt-3 mb-3">
<% if flash[:alert] %>
<div class="alert alert-danger" role="alert">
<%= flash[:alert] %><% flash.delete(:alert) %>

View file

@ -1,5 +1,5 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container">
<div class="container-fluid">
<a class="navbar-brand" href="<%= root_path %>">
<%= tag.i(class: "bi bi-universal-access") %>
a11ydive
@ -12,12 +12,21 @@
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<% @navbar_items.each do |navbar_item| %>
<li class="nav-item">
<a class="nav-link <%= @nav_path.to_s == navbar_item[:path].to_s && "active" %>" <%= @nav_path.to_s == navbar_item[:path].to_s && "aria-current=\"page\"" %> href="<%= url_for(navbar_item[:path]) %>">
<% if navbar_item[:icon] %>
<span aria-hidden="true" class="bi-<%= navbar_item[:icon] %>"></span>
<% if navbar_item[:method].present? && navbar_item[:method] != :get %>
<%= button_to(navbar_item[:path], method: navbar_item[:method], class: "nav-link") do %>
<% if navbar_item[:icon] %>
<span aria-hidden="true" class="bi-<%= navbar_item[:icon] %>"></span>
<% end %>
<%= navbar_item[:label] %>
<% end %>
<%= navbar_item[:label] %>
</a>
<% else %>
<a class="nav-link <%= (navbar_item[:active] || @nav_path.to_s == navbar_item[:path].to_s) && "active" %>" <%= @nav_path.to_s == navbar_item[:path].to_s && "aria-current=\"page\"" %> href="<%= url_for(navbar_item[:path]) %>">
<% if navbar_item[:icon] %>
<span aria-hidden="true" class="bi-<%= navbar_item[:icon] %>"></span>
<% end %>
<%= navbar_item[:label] %>
</a>
<% end %>
</li>
<% end %>
<%# <li class="nav-item">

View file

@ -0,0 +1,27 @@
<div class="col-auto px-0">
<div id="sidebar" class="collapse collapse-horizontal show border-0">
<div id="sidebar-nav" class="list-group border-0 rounded-0 text-sm-start min-vh-100">
<% 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 %>
<% if sidebar_item[:icon] %>
<span aria-hidden="true" class="bi-<%= sidebar_item[:icon] %>"></span>
<% end %>
<%= sidebar_item[:label] %>
<% end %>
<% else %>
<a class="list-group-item border-0 d-inline-block text-truncate <%= (sidebar_item[:active] || @nav_path.to_s == sidebar_item[:path].to_s) && "active" %>" <%= @nav_path.to_s == sidebar_item[:path].to_s && "aria-current=\"page\"" %> href="<%= url_for(sidebar_item[:path]) %>">
<% if sidebar_item[:icon] %>
<span aria-hidden="true" class="bi-<%= sidebar_item[:icon] %>"></span>
<% end %>
<%= sidebar_item[:label] %>
</a>
<% end %>
<% end %>
<% else %>
<div class="me-auto"></div>
<% end %>
</div>
</div>
</div>

View file

@ -12,12 +12,19 @@
</head>
<body class="d-flex flex-column min-vh-100">
<%= render partial: "layouts/sidebar" %>
<%= render partial: "layouts/navigation" %>
<%= render partial: "layouts/flash" %>
<main class="mt-3 container">
<%= yield %>
</main>
<footer class="container mt-auto"><%= Rails.configuration.build_version && "Version: #{Rails.configuration.build_version}" %>
<div class="container-fluid">
<div class="row flex-nowrap">
<%= render partial: "layouts/sidebar" %>
<main class="col ps-md-2 pt-2 <%= "border-start" if sidebar? %>">
<%= render partial: "layouts/flash" %>
<% if false && sidebar? %>
<a href="#" data-bs-target="#sidebar" data-bs-toggle="collapse" class="p-1 text-decoration-none"><i class="bi bi-list bi-lg py-2 p-1"></i> Menu</a>
<% end %>
<%= yield %>
</main>
</div>
</div>
<footer class="container-fluid mt-auto"><%= Rails.configuration.build_version && "Version: #{Rails.configuration.build_version}" %>
</body>
</html>

View file

@ -0,0 +1,26 @@
<%= form_with url: rodauth.login_path, method: :post, data: { turbo: false } do |form| %>
<% if rodauth.skip_login_field_on_login? %>
<div class="form-group mb-3">
<%= 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" %>
</div>
<% else %>
<div class="form-group mb-3">
<%= 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) %>
</div>
<% end %>
<% unless rodauth.skip_password_field_on_login? %>
<div class="form-group mb-3">
<%= 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) %>
</div>
<% end %>
<div class="form-group mb-3">
<%= form.submit rodauth.login_button, class: "btn btn-primary" %>
</div>
<% end %>

View file

@ -0,0 +1,9 @@
<% unless rodauth.login_form_footer_links.empty? %>
<%== rodauth.login_form_footer_links_heading %>
<ul>
<% rodauth.login_form_footer_links.sort.each do |_, link, text| %>
<li><%= link_to text, link %></li>
<% end %>
</ul>
<% end %>

View file

@ -0,0 +1,28 @@
<h1>Passwort ändern</h1>
<%= form_with url: rodauth.change_password_path, method: :post, data: { turbo: false } do |form| %>
<% if rodauth.change_password_requires_password? %>
<div class="form-group mb-3">
<%= 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) %>
</div>
<% end %>
<div class="form-group mb-3">
<%= 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) %>
</div>
<% if rodauth.require_password_confirmation? %>
<div class="form-group mb-3">
<%= 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) %>
</div>
<% end %>
<div class="form-group mb-3">
<%= form.submit rodauth.change_password_button, class: "btn btn-primary" %>
</div>
<% end %>

View file

@ -0,0 +1,3 @@
<%== rodauth.login_form_header %>
<%= render "login_form" %>
<%== rodauth.login_form_footer %>

View file

@ -0,0 +1,14 @@
<%= form_with url: rodauth.logout_path, method: :post, data: { turbo: false } do |form| %>
<% if rodauth.features.include?(:active_sessions) %>
<div class="form-group mb-3">
<div class="form-check">
<%= 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" %>
</div>
</div>
<% end %>
<div class="form-group mb-3">
<%= form.submit rodauth.logout_button, class: "btn btn-warning" %>
</div>
<% end %>

View file

@ -0,0 +1,3 @@
<%== rodauth.login_form_header %>
<%== rodauth.render_multi_phase_login_forms %>
<%== rodauth.login_form_footer %>

View file

@ -0,0 +1,3 @@
<h1><%= current_account.email %></h1>
<pre><%= JSON.pretty_generate current_account.attributes %></pre>

View file

@ -0,0 +1,22 @@
<%= form_with url: rodauth.remember_path, method: :post, data: { turbo: false } do |form| %>
<fieldset class="form-group mb-3">
<div class="form-check">
<%= 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" %>
</div>
<div class="form-check">
<%= 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" %>
</div>
<div class="form-check">
<%= 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" %>
</div>
</fieldset>
<div class="form-group mb-3">
<%= form.submit rodauth.remember_button, class: "btn btn-primary" %>
</div>
<% end %>