This commit is contained in:
20
app/controllers/accounts/custom_styles_controller.rb
Normal file
20
app/controllers/accounts/custom_styles_controller.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
class Accounts::CustomStylesController < ApplicationController
|
||||
before_action :ensure_can_administer, :set_account
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update!(account_params)
|
||||
redirect_to edit_account_custom_styles_url
|
||||
end
|
||||
|
||||
private
|
||||
def set_account
|
||||
@account = Current.account
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(:custom_styles)
|
||||
end
|
||||
end
|
||||
8
app/controllers/accounts/join_codes_controller.rb
Normal file
8
app/controllers/accounts/join_codes_controller.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
class Accounts::JoinCodesController < ApplicationController
|
||||
before_action :ensure_can_administer
|
||||
|
||||
def create
|
||||
Current.account.reset_join_code
|
||||
redirect_to users_path
|
||||
end
|
||||
end
|
||||
25
app/controllers/action_text/markdown/uploads_controller.rb
Normal file
25
app/controllers/action_text/markdown/uploads_controller.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
class ActionText::Markdown::UploadsController < ApplicationController
|
||||
allow_unauthenticated_access only: :show
|
||||
|
||||
before_action do
|
||||
ActiveStorage::Current.url_options = { protocol: request.protocol, host: request.host, port: request.port }
|
||||
end
|
||||
|
||||
def create
|
||||
@record = GlobalID::Locator.locate_signed params[:record_gid]
|
||||
|
||||
@markdown = @record.safe_markdown_attribute params[:attribute_name]
|
||||
@markdown.uploads.attach [ params[:file] ]
|
||||
@markdown.save!
|
||||
|
||||
@upload = @markdown.uploads.attachments.last
|
||||
|
||||
render :create, status: :created, formats: :json
|
||||
end
|
||||
|
||||
def show
|
||||
@attachment = ActiveStorage::Attachment.find_by! slug: "#{params[:slug]}.#{params[:format]}"
|
||||
expires_in 1.year, public: true
|
||||
redirect_to @attachment.url
|
||||
end
|
||||
end
|
||||
6
app/controllers/application_controller.rb
Normal file
6
app/controllers/application_controller.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include Authentication, Authorization, VersionHeaders
|
||||
|
||||
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
|
||||
allow_browser versions: :modern
|
||||
end
|
||||
14
app/controllers/books/bookmarks_controller.rb
Normal file
14
app/controllers/books/bookmarks_controller.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
class Books::BookmarksController < ApplicationController
|
||||
allow_unauthenticated_access
|
||||
|
||||
include BookScoped
|
||||
|
||||
def show
|
||||
@leaf = @book.leaves.active.find_by(id: last_read_leaf_id) if last_read_leaf_id.present?
|
||||
end
|
||||
|
||||
private
|
||||
def last_read_leaf_id
|
||||
cookies["reading_progress_#{@book.id}"]
|
||||
end
|
||||
end
|
||||
19
app/controllers/books/leaves/moves_controller.rb
Normal file
19
app/controllers/books/leaves/moves_controller.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
class Books::Leaves::MovesController < ApplicationController
|
||||
include BookScoped
|
||||
|
||||
before_action :ensure_editable
|
||||
|
||||
def create
|
||||
leaf, *followed_by = leaves
|
||||
leaf.move_to_position(position, followed_by: followed_by)
|
||||
end
|
||||
|
||||
private
|
||||
def position
|
||||
params[:position].to_i
|
||||
end
|
||||
|
||||
def leaves
|
||||
@book.leaves.find(Array(params[:id]))
|
||||
end
|
||||
end
|
||||
21
app/controllers/books/publications_controller.rb
Normal file
21
app/controllers/books/publications_controller.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
class Books::PublicationsController < ApplicationController
|
||||
include BookScoped
|
||||
|
||||
before_action :ensure_editable, only: %i[ edit update ]
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@book.update! book_params
|
||||
redirect_to book_slug_url(@book)
|
||||
end
|
||||
|
||||
private
|
||||
def book_params
|
||||
params.require(:book).permit(:published, :slug)
|
||||
end
|
||||
end
|
||||
78
app/controllers/books_controller.rb
Normal file
78
app/controllers/books_controller.rb
Normal file
@@ -0,0 +1,78 @@
|
||||
class BooksController < ApplicationController
|
||||
allow_unauthenticated_access only: %i[ index show ]
|
||||
|
||||
before_action :ensure_index_is_not_empty, only: :index
|
||||
before_action :set_book, only: %i[ show edit update destroy ]
|
||||
before_action :set_users, only: %i[ new edit ]
|
||||
before_action :ensure_editable, only: %i[ edit update destroy ]
|
||||
|
||||
def index
|
||||
@books = Book.accessable_or_published.ordered
|
||||
end
|
||||
|
||||
def new
|
||||
@book = Book.new
|
||||
end
|
||||
|
||||
def create
|
||||
book = Book.create! book_params
|
||||
update_accesses(book)
|
||||
|
||||
redirect_to book_slug_url(book)
|
||||
end
|
||||
|
||||
def show
|
||||
@leaves = @book.leaves.active.with_leafables.positioned
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@book.update(book_params)
|
||||
update_accesses(@book)
|
||||
remove_cover if params[:remove_cover] == "true"
|
||||
|
||||
redirect_to book_slug_url(@book)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@book.destroy
|
||||
|
||||
redirect_to root_url
|
||||
end
|
||||
|
||||
private
|
||||
def set_book
|
||||
@book = Book.accessable_or_published.find params[:id]
|
||||
end
|
||||
|
||||
def set_users
|
||||
@users = User.active.ordered
|
||||
end
|
||||
|
||||
def ensure_editable
|
||||
head :forbidden unless @book.editable?
|
||||
end
|
||||
|
||||
def ensure_index_is_not_empty
|
||||
if !signed_in? && Book.published.none?
|
||||
require_authentication
|
||||
end
|
||||
end
|
||||
|
||||
def book_params
|
||||
params.require(:book).permit(:title, :subtitle, :author, :cover, :remove_cover, :everyone_access, :theme)
|
||||
end
|
||||
|
||||
def update_accesses(book)
|
||||
editors = [ Current.user.id, *params[:editor_ids]&.map(&:to_i) ]
|
||||
readers = [ Current.user.id, *params[:reader_ids]&.map(&:to_i) ]
|
||||
|
||||
book.update_access(editors: editors, readers: readers)
|
||||
end
|
||||
|
||||
def remove_cover
|
||||
@book.cover.purge
|
||||
end
|
||||
end
|
||||
0
app/controllers/concerns/.keep
Normal file
0
app/controllers/concerns/.keep
Normal file
80
app/controllers/concerns/authentication.rb
Normal file
80
app/controllers/concerns/authentication.rb
Normal file
@@ -0,0 +1,80 @@
|
||||
module Authentication
|
||||
extend ActiveSupport::Concern
|
||||
include SessionLookup
|
||||
|
||||
included do
|
||||
before_action :require_authentication
|
||||
helper_method :signed_in?
|
||||
|
||||
protect_from_forgery with: :exception, unless: -> { authenticated_by.bot_key? }
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def require_unauthenticated_access(**options)
|
||||
allow_unauthenticated_access **options
|
||||
before_action :redirect_signed_in_user_to_root, **options
|
||||
end
|
||||
|
||||
def allow_unauthenticated_access(**options)
|
||||
skip_before_action :require_authentication, **options
|
||||
before_action :restore_authentication, **options
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def signed_in?
|
||||
Current.user.present?
|
||||
end
|
||||
|
||||
def require_authentication
|
||||
restore_authentication || request_authentication
|
||||
end
|
||||
|
||||
def restore_authentication
|
||||
if session = find_session_by_cookie
|
||||
resume_session session
|
||||
end
|
||||
end
|
||||
|
||||
def request_authentication
|
||||
session[:return_to_after_authenticating] = request.url
|
||||
redirect_to new_session_url
|
||||
end
|
||||
|
||||
def redirect_signed_in_user_to_root
|
||||
redirect_to root_url if signed_in?
|
||||
end
|
||||
|
||||
def start_new_session_for(user)
|
||||
user.sessions.start!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
|
||||
authenticated_as session
|
||||
end
|
||||
end
|
||||
|
||||
def resume_session(session)
|
||||
session.resume user_agent: request.user_agent, ip_address: request.remote_ip
|
||||
authenticated_as session
|
||||
end
|
||||
|
||||
def authenticated_as(session)
|
||||
Current.user = session.user
|
||||
set_authenticated_by(:session)
|
||||
cookies.signed.permanent[:session_token] = { value: session.token, httponly: true, same_site: :lax }
|
||||
end
|
||||
|
||||
def post_authenticating_url
|
||||
session.delete(:return_to_after_authenticating) || root_url
|
||||
end
|
||||
|
||||
def reset_authentication
|
||||
cookies.delete(:session_token)
|
||||
end
|
||||
|
||||
def set_authenticated_by(method)
|
||||
@authenticated_by = method.to_s.inquiry
|
||||
end
|
||||
|
||||
def authenticated_by
|
||||
@authenticated_by ||= "".inquiry
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
module Authentication::SessionLookup
|
||||
def find_session_by_cookie
|
||||
if token = cookies.signed[:session_token]
|
||||
Session.find_by(token: token)
|
||||
end
|
||||
end
|
||||
end
|
||||
14
app/controllers/concerns/book_scoped.rb
Normal file
14
app/controllers/concerns/book_scoped.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module BookScoped extend ActiveSupport::Concern
|
||||
included do
|
||||
before_action :set_book
|
||||
end
|
||||
|
||||
private
|
||||
def set_book
|
||||
@book = Book.accessable_or_published.find(params[:book_id])
|
||||
end
|
||||
|
||||
def ensure_editable
|
||||
head :forbidden unless @book.editable?
|
||||
end
|
||||
end
|
||||
10
app/controllers/concerns/page_leaf_scoped.rb
Normal file
10
app/controllers/concerns/page_leaf_scoped.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
module PageLeafScoped extend ActiveSupport::Concern
|
||||
included do
|
||||
before_action :set_leaf
|
||||
end
|
||||
|
||||
private
|
||||
def set_leaf
|
||||
@leaf = Current.user.leaves.find(params[:page_id])
|
||||
end
|
||||
end
|
||||
37
app/controllers/concerns/set_book_leaf.rb
Normal file
37
app/controllers/concerns/set_book_leaf.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
module SetBookLeaf
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_book
|
||||
before_action :set_leaf, :set_leafable, only: %i[ show edit update destroy ]
|
||||
end
|
||||
|
||||
private
|
||||
def set_book
|
||||
@book = Book.accessable_or_published.find(params[:book_id])
|
||||
end
|
||||
|
||||
def set_leaf
|
||||
@leaf = @book.leaves.active.find(params[:id])
|
||||
end
|
||||
|
||||
def set_leafable
|
||||
instance_variable_set "@#{instance_name}", @leaf.leafable
|
||||
end
|
||||
|
||||
def ensure_editable
|
||||
head :forbidden unless @book.editable?
|
||||
end
|
||||
|
||||
def model_class
|
||||
controller_leafable_name.constantize
|
||||
end
|
||||
|
||||
def instance_name
|
||||
controller_leafable_name.underscore
|
||||
end
|
||||
|
||||
def controller_leafable_name
|
||||
self.class.to_s.remove("Controller").demodulize.singularize
|
||||
end
|
||||
end
|
||||
12
app/controllers/concerns/user_scoped.rb
Normal file
12
app/controllers/concerns/user_scoped.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module UserScoped
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_user
|
||||
end
|
||||
|
||||
private
|
||||
def set_user
|
||||
@user = User.active.find(params[:user_id])
|
||||
end
|
||||
end
|
||||
13
app/controllers/concerns/version_headers.rb
Normal file
13
app/controllers/concerns/version_headers.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
module VersionHeaders
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_version_headers
|
||||
end
|
||||
|
||||
private
|
||||
def set_version_headers
|
||||
response.headers["X-Version"] = Rails.application.config.app_version
|
||||
response.headers["X-Rev"] = Rails.application.config.git_revision
|
||||
end
|
||||
end
|
||||
25
app/controllers/first_runs_controller.rb
Normal file
25
app/controllers/first_runs_controller.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
class FirstRunsController < ApplicationController
|
||||
allow_unauthenticated_access
|
||||
|
||||
before_action :prevent_running_after_setup
|
||||
|
||||
def show
|
||||
@user = User.new
|
||||
end
|
||||
|
||||
def create
|
||||
user = FirstRun.create!(user_params)
|
||||
start_new_session_for user
|
||||
|
||||
redirect_to root_url
|
||||
end
|
||||
|
||||
private
|
||||
def prevent_running_after_setup
|
||||
redirect_to root_url if User.any?
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:name, :email_address, :password)
|
||||
end
|
||||
end
|
||||
69
app/controllers/leafables_controller.rb
Normal file
69
app/controllers/leafables_controller.rb
Normal file
@@ -0,0 +1,69 @@
|
||||
class LeafablesController < ApplicationController
|
||||
allow_unauthenticated_access only: :show
|
||||
|
||||
include SetBookLeaf
|
||||
|
||||
before_action :ensure_editable, except: :show
|
||||
before_action :broadcast_being_edited_indicator, only: :update
|
||||
|
||||
def new
|
||||
@leafable = new_leafable
|
||||
end
|
||||
|
||||
def create
|
||||
@leaf = @book.press new_leafable, leaf_params
|
||||
position_new_leaf @leaf
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@leaf.edit leafable_params: leafable_params, leaf_params: leaf_params
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream { render }
|
||||
format.html { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@leaf.trashed!
|
||||
|
||||
respond_to do |format|
|
||||
format.turbo_stream { render }
|
||||
format.html { redirect_to book_slug_url(@book) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def leaf_params
|
||||
default_leaf_params.merge params.fetch(:leaf, {}).permit(:title)
|
||||
end
|
||||
|
||||
def default_leaf_params
|
||||
{ title: new_leafable.model_name.human }
|
||||
end
|
||||
|
||||
def new_leafable
|
||||
raise NotImplementedError.new "Implement in subclass"
|
||||
end
|
||||
|
||||
def leafable_params
|
||||
raise NotImplementedError.new "Implement in subclass"
|
||||
end
|
||||
|
||||
def position_new_leaf(leaf)
|
||||
if position = params[:position]&.to_i
|
||||
leaf.move_to_position position
|
||||
end
|
||||
end
|
||||
|
||||
def broadcast_being_edited_indicator
|
||||
Turbo::StreamsChannel.broadcast_render_later_to @leaf, :being_edited,
|
||||
partial: "leaves/being_edited_by", locals: { leaf: @leaf, user: Current.user }
|
||||
end
|
||||
end
|
||||
17
app/controllers/pages/edits_controller.rb
Normal file
17
app/controllers/pages/edits_controller.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
class Pages::EditsController < ApplicationController
|
||||
include PageLeafScoped
|
||||
|
||||
before_action :set_edit
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
private
|
||||
def set_edit
|
||||
if params[:id] == "latest"
|
||||
@edit = @leaf.edits.last
|
||||
else
|
||||
@edit = @leaf.edits.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
20
app/controllers/pages_controller.rb
Normal file
20
app/controllers/pages_controller.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
class PagesController < LeafablesController
|
||||
before_action :forget_reading_progress, except: :show
|
||||
|
||||
private
|
||||
def forget_reading_progress
|
||||
cookies.delete "reading_progress_#{@book.id}"
|
||||
end
|
||||
|
||||
def default_leaf_params
|
||||
{ title: "Untitled" }
|
||||
end
|
||||
|
||||
def new_leafable
|
||||
Page.new leafable_params
|
||||
end
|
||||
|
||||
def leafable_params
|
||||
params.fetch(:page, {}).permit(:body)
|
||||
end
|
||||
end
|
||||
10
app/controllers/pictures_controller.rb
Normal file
10
app/controllers/pictures_controller.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
class PicturesController < LeafablesController
|
||||
private
|
||||
def new_leafable
|
||||
Picture.new leafable_params
|
||||
end
|
||||
|
||||
def leafable_params
|
||||
params.fetch(:picture, {}).permit(:image, :caption)
|
||||
end
|
||||
end
|
||||
11
app/controllers/qr_code_controller.rb
Normal file
11
app/controllers/qr_code_controller.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class QrCodeController < ApplicationController
|
||||
allow_unauthenticated_access
|
||||
|
||||
def show
|
||||
qr_code_link = QrCodeLink.from_signed(params[:id])
|
||||
svg = RQRCode::QRCode.new(qr_code_link.url).as_svg(viewbox: true, fill: :white, color: :black)
|
||||
|
||||
expires_in 1.year, public: true
|
||||
render plain: svg, content_type: "image/svg+xml"
|
||||
end
|
||||
end
|
||||
15
app/controllers/sections_controller.rb
Normal file
15
app/controllers/sections_controller.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
class SectionsController < LeafablesController
|
||||
private
|
||||
def new_leafable
|
||||
Section.new leafable_params
|
||||
end
|
||||
|
||||
def leafable_params
|
||||
params.fetch(:section, {}).permit(:body, :theme)
|
||||
.with_defaults(body: default_body)
|
||||
end
|
||||
|
||||
def default_body
|
||||
params.fetch(:leaf, {})[:title]
|
||||
end
|
||||
end
|
||||
15
app/controllers/sessions/transfers_controller.rb
Normal file
15
app/controllers/sessions/transfers_controller.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
class Sessions::TransfersController < ApplicationController
|
||||
allow_unauthenticated_access
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
if user = User.active.find_by_transfer_id(params[:id])
|
||||
start_new_session_for user
|
||||
redirect_to post_authenticating_url
|
||||
else
|
||||
head :bad_request
|
||||
end
|
||||
end
|
||||
end
|
||||
34
app/controllers/sessions_controller.rb
Normal file
34
app/controllers/sessions_controller.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
class SessionsController < ApplicationController
|
||||
allow_unauthenticated_access only: %i[ new create ]
|
||||
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { render_rejection :too_many_requests }
|
||||
|
||||
before_action :ensure_user_exists, only: :new
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
if user = User.active.authenticate_by(email_address: params[:email_address], password: params[:password])
|
||||
start_new_session_for user
|
||||
redirect_to post_authenticating_url
|
||||
else
|
||||
render_rejection :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
reset_authentication
|
||||
|
||||
redirect_to root_url
|
||||
end
|
||||
|
||||
private
|
||||
def ensure_user_exists
|
||||
redirect_to first_run_url if User.none?
|
||||
end
|
||||
|
||||
def render_rejection(status)
|
||||
flash[:alert] = "Too many requests or unauthorized."
|
||||
render :new, status: status
|
||||
end
|
||||
end
|
||||
21
app/controllers/users/profiles_controller.rb
Normal file
21
app/controllers/users/profiles_controller.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
class Users::ProfilesController < ApplicationController
|
||||
include UserScoped
|
||||
|
||||
before_action :ensure_current_user, only: %i[ edit update ]
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@user.update!(user_params)
|
||||
redirect_to users_url
|
||||
end
|
||||
|
||||
private
|
||||
def user_params
|
||||
params.require(:user).permit(:name, :email_address, :password)
|
||||
end
|
||||
end
|
||||
51
app/controllers/users_controller.rb
Normal file
51
app/controllers/users_controller.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
class UsersController < ApplicationController
|
||||
require_unauthenticated_access only: %i[ new create ]
|
||||
|
||||
before_action :verify_join_code, only: %i[ new create ]
|
||||
before_action :ensure_can_administer, only: %i[ update destroy ]
|
||||
before_action :set_user, only: %i[ update destroy ]
|
||||
|
||||
|
||||
def index
|
||||
@users = User.active
|
||||
end
|
||||
|
||||
def new
|
||||
@user = User.new
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.create!(user_params)
|
||||
start_new_session_for @user
|
||||
redirect_to root_url
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
redirect_to new_session_url(email_address: user_params[:email_address])
|
||||
end
|
||||
|
||||
def update
|
||||
@user.update(role_params)
|
||||
redirect_to users_url
|
||||
end
|
||||
|
||||
def destroy
|
||||
@user.deactivate
|
||||
redirect_to users_url
|
||||
end
|
||||
|
||||
private
|
||||
def role_params
|
||||
{ role: params.require(:user)[:role].presence_in(%w[ member administrator ]) || "member" }
|
||||
end
|
||||
|
||||
def set_user
|
||||
@user = User.active.find(params[:id])
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:name, :email_address, :password)
|
||||
end
|
||||
|
||||
def verify_join_code
|
||||
head :not_found if Current.account.join_code != params[:join_code]
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user