This commit is contained in:
2025-11-07 13:34:32 -08:00
commit 1e8c5a972b
436 changed files with 11000 additions and 0 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

View 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

View File

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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