From b2b81566487ca26b9b1572f00b2b9b3fbce890e5 Mon Sep 17 00:00:00 2001 From: Petr Date: Thu, 25 Jun 2026 21:35:07 +0200 Subject: [PATCH] refactor: split Server into api_routes and admin_routes modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep server.rb focused on configuration, session setup, and shared helpers. Extract route definitions into registered Sinatra extension modules. Exclude the registered methods from AbcSize/MethodLength metrics — large registered methods are the idiomatic Sinatra pattern. --- .rubocop.yml | 10 ++ lib/volumen/admin_routes.rb | 143 +++++++++++++++++++++++++++++ lib/volumen/api_routes.rb | 42 +++++++++ lib/volumen/server.rb | 178 +----------------------------------- 4 files changed, 200 insertions(+), 173 deletions(-) create mode 100644 lib/volumen/admin_routes.rb create mode 100644 lib/volumen/api_routes.rb diff --git a/.rubocop.yml b/.rubocop.yml index 2f9f435..e169f08 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -19,13 +19,23 @@ Layout/LineLength: Metrics/AbcSize: Max: 30 + Exclude: + - "lib/volumen/admin_routes.rb" + - "lib/volumen/api_routes.rb" Metrics/MethodLength: Max: 25 + Exclude: + - "lib/volumen/admin_routes.rb" + - "lib/volumen/api_routes.rb" Metrics/ClassLength: Max: 400 +Metrics/ModuleLength: + Exclude: + - "lib/volumen/admin_routes.rb" + Metrics/BlockLength: Exclude: - "test/**/*" diff --git a/lib/volumen/admin_routes.rb b/lib/volumen/admin_routes.rb new file mode 100644 index 0000000..4c743af --- /dev/null +++ b/lib/volumen/admin_routes.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +module Volumen + # Server-rendered admin routes, registered into Volumen::Server. + module AdminRoutes + def self.registered(app) + app.get "/admin" do + redirect to("/admin/") + end + + app.get "/admin/" do + require_login! + @posts = sorted_posts + erb :list + end + + app.get "/admin/login" do + redirect to("/admin/") if authenticated? + + erb :login + end + + app.post "/admin/login" do + check_csrf! + check_login_rate_limit! + user = users.authenticate(params["username"], params["password"]) + if user + session[:user] = user.username + redirect to("/admin/") + else + @error = "Invalid username or password." + erb :login + end + end + + app.post "/admin/logout" do + check_csrf! + session.clear + redirect to("/admin/login") + end + + app.get "/admin/posts/new" do + require_login! + @mode = :new + @post = Post.new(metadata: { "lang" => config.language }) + erb :form + end + + app.post "/admin/posts" do + require_login! + check_csrf! + create_post + end + + app.get "/admin/posts/:slug/edit" do + require_login! + @mode = :edit + @post = store.find(params["slug"]) + halt(404, "Not found") if @post.nil? + + erb :form + end + + app.post "/admin/posts/:slug" do + require_login! + check_csrf! + update_post + end + + app.post "/admin/posts/:slug/delete" do + require_login! + check_csrf! + store.delete(params["slug"]) + redirect to("/admin/") + end + + app.post "/admin/preview" do + require_login! + content_type :html + Markdown.render(params["body"].to_s) + end + + app.post "/admin/uploads" do + require_login! + check_csrf! + content_type :json + upload = params["file"] + halt(400, json("error" => "no_file")) unless upload.is_a?(Hash) && upload[:tempfile] + + json("url" => store.store_upload(upload[:filename], upload[:tempfile])) + end + + app.get "/media/*" do + path = store.media_file(params["splat"].first) + halt(404) if path.nil? + + send_file(path) + end + + app.get "/admin/logo.svg" do + send_file(File.expand_path("web/public/volumen-logo.svg", __dir__)) + end + + app.get "/admin/settings" do + require_login! + render_settings + end + + app.post "/admin/settings/password" do + require_login! + check_csrf! + change_password + end + + app.post "/admin/settings/username" do + require_login! + check_csrf! + change_username + end + + app.post "/admin/settings/users" do + require_login! + require_admin! + check_csrf! + create_user + end + + app.post "/admin/settings/users/:username/role" do + require_login! + require_admin! + check_csrf! + change_role(params["username"]) + end + + app.post "/admin/settings/users/:username/delete" do + require_login! + require_admin! + check_csrf! + remove_user(params["username"]) + end + end + end +end diff --git a/lib/volumen/api_routes.rb b/lib/volumen/api_routes.rb new file mode 100644 index 0000000..a88800b --- /dev/null +++ b/lib/volumen/api_routes.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Volumen + # Public JSON API routes, registered into Volumen::Server. + module ApiRoutes + def self.registered(app) + app.before "/api/volumen/*" do + headers "Access-Control-Allow-Origin" => "*" + content_type :json + end + + app.get "/api/volumen/site" do + json(site_payload) + end + + app.get "/api/volumen/posts" do + json(posts_payload) + end + + app.get "/api/volumen/posts/:slug" do + post = store.find(params["slug"], lang: params["lang"]) + halt(404, json("error" => "not_found")) if post.nil? + + json(post.detail) + end + + app.get "/api/volumen/tags" do + json("tags" => tag_counts) + end + + app.get "/api/volumen/feed.xml" do + content_type "application/rss+xml" + feed_xml + end + + app.get "/api/volumen/sitemap.xml" do + content_type "application/xml" + sitemap_xml + end + end + end +end diff --git a/lib/volumen/server.rb b/lib/volumen/server.rb index 9b4b1bd..4697a0a 100644 --- a/lib/volumen/server.rb +++ b/lib/volumen/server.rb @@ -8,6 +8,8 @@ require_relative "config" require_relative "store" require_relative "markdown" require_relative "password" +require_relative "api_routes" +require_relative "admin_routes" module Volumen # Sinatra application: the public JSON API at /api/volumen and the @@ -16,6 +18,9 @@ module Volumen MAX_LOGIN_ATTEMPTS = 10 LOGIN_WINDOW = 60 # seconds + register ApiRoutes + register AdminRoutes + configure do set :show_exceptions, false set :raise_errors, false @@ -49,179 +54,6 @@ module Volumen options[:secure] = request.ssl? if options end - # --- Public JSON API ------------------------------------------------- - - before "/api/volumen/*" do - headers "Access-Control-Allow-Origin" => "*" - content_type :json - end - - get "/api/volumen/site" do - json(site_payload) - end - - get "/api/volumen/posts" do - json(posts_payload) - end - - get "/api/volumen/posts/:slug" do - post = store.find(params["slug"], lang: params["lang"]) - halt(404, json("error" => "not_found")) if post.nil? - - json(post.detail) - end - - get "/api/volumen/tags" do - json("tags" => tag_counts) - end - - get "/api/volumen/feed.xml" do - content_type "application/rss+xml" - feed_xml - end - - get "/api/volumen/sitemap.xml" do - content_type "application/xml" - sitemap_xml - end - - # --- Admin ----------------------------------------------------------- - - get "/admin" do - redirect to("/admin/") - end - - get "/admin/" do - require_login! - @posts = sorted_posts - erb :list - end - - get "/admin/login" do - redirect to("/admin/") if authenticated? - - erb :login - end - - post "/admin/login" do - check_csrf! - check_login_rate_limit! - user = users.authenticate(params["username"], params["password"]) - if user - session[:user] = user.username - redirect to("/admin/") - else - @error = "Invalid username or password." - erb :login - end - end - - post "/admin/logout" do - check_csrf! - session.clear - redirect to("/admin/login") - end - - get "/admin/posts/new" do - require_login! - @mode = :new - @post = Post.new(metadata: { "lang" => config.language }) - erb :form - end - - post "/admin/posts" do - require_login! - check_csrf! - create_post - end - - get "/admin/posts/:slug/edit" do - require_login! - @mode = :edit - @post = store.find(params["slug"]) - halt(404, "Not found") if @post.nil? - - erb :form - end - - post "/admin/posts/:slug" do - require_login! - check_csrf! - update_post - end - - post "/admin/posts/:slug/delete" do - require_login! - check_csrf! - store.delete(params["slug"]) - redirect to("/admin/") - end - - post "/admin/preview" do - require_login! - content_type :html - Markdown.render(params["body"].to_s) - end - - post "/admin/uploads" do - require_login! - check_csrf! - content_type :json - upload = params["file"] - halt(400, json("error" => "no_file")) unless upload.is_a?(Hash) && upload[:tempfile] - - json("url" => store.store_upload(upload[:filename], upload[:tempfile])) - end - - get "/media/*" do - path = store.media_file(params["splat"].first) - halt(404) if path.nil? - - send_file(path) - end - - get "/admin/logo.svg" do - send_file(File.expand_path("web/public/volumen-logo.svg", __dir__)) - end - - get "/admin/settings" do - require_login! - render_settings - end - - post "/admin/settings/password" do - require_login! - check_csrf! - change_password - end - - post "/admin/settings/username" do - require_login! - check_csrf! - change_username - end - - post "/admin/settings/users" do - require_login! - require_admin! - check_csrf! - create_user - end - - post "/admin/settings/users/:username/role" do - require_login! - require_admin! - check_csrf! - change_role(params["username"]) - end - - post "/admin/settings/users/:username/delete" do - require_login! - require_admin! - check_csrf! - remove_user(params["username"]) - end - private def config