discourse/config/routes.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1652 lines
64 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2013-12-24 07:50:36 +08:00
require "sidekiq/web"
require "mini_scheduler/web"
2013-02-06 03:16:51 +08:00
# The following constants have been replaced with `RouteFormat` and are deprecated.
USERNAME_ROUTE_FORMAT = /[%\w.\-]+?/ unless defined?(USERNAME_ROUTE_FORMAT)
BACKUP_ROUTE_FORMAT = /.+\.(sql\.gz|tar\.gz|tgz)/i unless defined?(BACKUP_ROUTE_FORMAT)
2013-02-06 03:16:51 +08:00
Discourse::Application.routes.draw do
def patch(*)
end # Disable PATCH requests
scope path: nil, constraints: { format: %r{(json|html|\*/\*)} } do
relative_url_root =
(
if (
defined?(Rails.configuration.relative_url_root) &&
Rails.configuration.relative_url_root
)
Rails.configuration.relative_url_root + "/"
else
"/"
end
)
2013-02-06 03:16:51 +08:00
match "/404", to: "exceptions#not_found", via: %i[get post]
2014-04-30 03:17:40 +08:00
get "/404-body" => "exceptions#not_found_body"
2020-06-05 10:49:31 +08:00
2022-10-20 01:10:06 +08:00
if Rails.env.test? || Rails.env.development?
get "/bootstrap/plugin-css-for-tests.css" => "bootstrap#plugin_css_for_tests"
end
2020-06-05 10:49:31 +08:00
# This is not a valid production route and is causing routing errors to be raised in
# the test env adding noise to the logs. Just handle it here so we eliminate the noise.
get "/favicon.ico", to: proc { [200, {}, [""]] } if Rails.env.test?
post "webhooks/aws" => "webhooks#aws"
2016-06-07 01:47:45 +08:00
post "webhooks/mailgun" => "webhooks#mailgun"
post "webhooks/mailjet" => "webhooks#mailjet"
post "webhooks/mailpace" => "webhooks#mailpace"
2016-06-13 18:31:01 +08:00
post "webhooks/mandrill" => "webhooks#mandrill"
get "webhooks/mandrill" => "webhooks#mandrill_head"
2020-02-11 23:09:07 +08:00
post "webhooks/postmark" => "webhooks#postmark"
post "webhooks/sendgrid" => "webhooks#sendgrid"
2016-09-27 13:13:34 +08:00
post "webhooks/sparkpost" => "webhooks#sparkpost"
2020-06-05 10:49:31 +08:00
scope path: nil, format: true, constraints: { format: :xml } do
resources :sitemap, only: [:index]
get "/sitemap_:page" => "sitemap#page", :page => /[1-9][0-9]*/
get "/sitemap_recent" => "sitemap#recent"
get "/news" => "sitemap#news"
end
scope path: nil, constraints: { format: /.*/ } do
if Rails.env.development?
mount Sidekiq::Web => "/sidekiq"
mount Logster::Web => "/logs"
else
# only allow sidekiq in master site
mount Sidekiq::Web => "/sidekiq", :constraints => AdminConstraint.new(require_master: true)
mount Logster::Web => "/logs", :constraints => AdminConstraint.new
2020-06-05 10:49:31 +08:00
end
end
resources :about, only: [:index] do
collection { get "live_post_counts" }
end
2014-08-12 04:59:00 +08:00
get "finish-installation" => "finish_installation#index"
get "finish-installation/register" => "finish_installation#register"
post "finish-installation/register" => "finish_installation#register"
get "finish-installation/confirm-email" => "finish_installation#confirm_email"
put "finish-installation/resend-email" => "finish_installation#resend_email"
2020-06-05 10:49:31 +08:00
get "pub/check-slug" => "published_pages#check_slug"
get "pub/by-topic/:topic_id" => "published_pages#details"
put "pub/by-topic/:topic_id" => "published_pages#upsert"
delete "pub/by-topic/:topic_id" => "published_pages#destroy"
get "pub/:slug" => "published_pages#show"
2020-06-05 10:49:31 +08:00
resources :directory_items, only: [:index]
2020-06-05 10:49:31 +08:00
get "site" => "site#site"
namespace :site do
get "settings"
get "custom_html"
get "banner"
get "emoji"
end
get "site/basic-info" => "site#basic_info"
2017-03-10 21:16:00 +08:00
get "site/statistics" => "site#statistics"
2013-12-24 07:50:36 +08:00
get "srv/status" => "forums#status"
2013-02-06 03:16:51 +08:00
2016-08-26 01:14:56 +08:00
get "wizard" => "wizard#index"
2016-09-08 06:04:01 +08:00
get "wizard/steps/:id" => "wizard#index"
2016-08-26 01:14:56 +08:00
put "wizard/steps/:id" => "steps#update"
namespace :admin, constraints: StaffConstraint.new do
2013-12-24 07:50:36 +08:00
get "" => "admin#index"
2013-02-06 03:16:51 +08:00
get "plugins" => "plugins#index"
get "plugins/:plugin_id" => "plugins#show"
get "plugins/:plugin_id/settings" => "plugins#show"
resources :site_settings, only: %i[index update], constraints: AdminConstraint.new do
2013-12-24 07:50:36 +08:00
collection { get "category/:id" => "site_settings#index" }
put "user_count" => "site_settings#user_count"
2013-11-14 03:02:47 +08:00
end
get "reports" => "reports#index"
get "reports/bulk" => "reports#bulk"
2013-12-24 07:50:36 +08:00
get "reports/:type" => "reports#show"
resources :groups, only: [:create] do
member do
delete "owners" => "groups#remove_owner"
put "primary" => "groups#set_primary"
end
end
resources :groups, only: [:destroy], constraints: AdminConstraint.new do
collection { put "automatic_membership_count" => "groups#automatic_membership_count" }
end
2013-05-08 13:20:38 +08:00
resources :users, id: RouteFormat.username, only: %i[index destroy] do
2013-02-06 03:16:51 +08:00
collection do
2017-04-27 11:02:59 +08:00
get "list" => "users#index"
2013-12-24 07:50:36 +08:00
get "list/:query" => "users#index"
get "ip-info" => "users#ip_info"
delete "delete-others-with-same-ip" => "users#delete_other_accounts_with_same_ip"
get "total-others-with-same-ip" => "users#total_other_accounts_with_same_ip"
2013-12-24 07:50:36 +08:00
put "approve-bulk" => "users#approve_bulk"
2020-06-05 10:49:31 +08:00
end
delete "penalty_history", constraints: AdminConstraint.new
2013-12-24 07:50:36 +08:00
put "suspend"
put "delete_posts_batch"
2013-12-24 07:50:36 +08:00
put "unsuspend"
put "revoke_admin", constraints: AdminConstraint.new
put "grant_admin", constraints: AdminConstraint.new
put "revoke_moderation", constraints: AdminConstraint.new
put "grant_moderation", constraints: AdminConstraint.new
put "approve"
2014-06-06 11:02:52 +08:00
post "log_out", constraints: AdminConstraint.new
2013-12-24 07:50:36 +08:00
put "activate"
put "deactivate"
2017-11-11 01:18:08 +08:00
put "silence"
put "unsilence"
2013-12-24 07:50:36 +08:00
put "trust_level"
put "trust_level_lock"
put "primary_group"
post "groups" => "users#add_group", :constraints => AdminConstraint.new
delete "groups/:group_id" => "users#remove_group", :constraints => AdminConstraint.new
get "badges"
2014-09-25 08:19:26 +08:00
get "leader_requirements" => "users#tl3_requirements"
get "tl3_requirements"
put "anonymize"
post "merge"
post "reset-bounce-score"
put "disable_second_factor"
delete "sso_record"
2013-02-06 03:16:51 +08:00
end
get "users/:id.json" => "users#show", :defaults => { format: "json" }
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
get "users/:id/:username" => "users#show",
:constraints => {
username: RouteFormat.username,
},
:as => :user_show
get "users/:id/:username/badges" => "users#show"
get "users/:id/:username/tl3_requirements" => "users#show"
2013-02-06 03:16:51 +08:00
post "users/sync_sso" => "users#sync_sso", :constraints => AdminConstraint.new
resources :impersonate, only: %i[index create], constraints: AdminConstraint.new
resources :email, only: [:index], constraints: AdminConstraint.new do
collection do
post "test"
2014-02-15 07:50:08 +08:00
get "sent"
get "skipped"
2016-05-03 05:15:32 +08:00
get "bounced"
get "received"
get "rejected"
get "/incoming/:id" => "email#incoming"
get "/incoming_from_bounced/:id" => "email#incoming_from_bounced"
2013-12-24 07:50:36 +08:00
get "preview-digest" => "email#preview_digest"
post "send-digest" => "email#send_digest"
get "smtp_should_reject"
post "handle_mail"
get "advanced-test"
post "advanced-test" => "email#advanced_test"
end
end
2020-06-05 10:49:31 +08:00
2013-12-24 07:50:36 +08:00
scope "/logs" do
resources :staff_action_logs, only: [:index]
get "staff_action_logs/:id/diff" => "staff_action_logs#diff"
resources :screened_emails, only: %i[index destroy]
resources :screened_ip_addresses, only: %i[index create update destroy]
resources :screened_urls, only: [:index]
2017-11-15 08:13:50 +08:00
resources :search_logs, only: [:index]
get "search_logs/term/" => "search_logs#term"
end
2013-08-02 09:30:13 +08:00
get "/logs" => "staff_action_logs#index"
2020-06-05 10:49:31 +08:00
# alias
get "/logs/watched_words", to: redirect(relative_url_root + "admin/customize/watched_words")
get "/logs/watched_words/*path",
to: redirect(relative_url_root + "admin/customize/watched_words/%{path}")
get "customize" => "color_schemes#index", :constraints => AdminConstraint.new
get "customize/themes" => "themes#index", :constraints => AdminConstraint.new
get "customize/components" => "themes#index", :constraints => AdminConstraint.new
get "customize/theme-components" => "themes#index", :constraints => AdminConstraint.new
get "customize/colors" => "color_schemes#index", :constraints => AdminConstraint.new
get "customize/colors/:id" => "color_schemes#index", :constraints => AdminConstraint.new
2015-07-15 20:54:28 +08:00
get "customize/permalinks" => "permalinks#index", :constraints => AdminConstraint.new
get "customize/embedding" => "embedding#show", :constraints => AdminConstraint.new
put "customize/embedding" => "embedding#update", :constraints => AdminConstraint.new
2020-06-05 10:49:31 +08:00
resources :themes,
only: %i[index create show update destroy],
constraints: AdminConstraint.new do
member do
get "preview" => "themes#preview"
get "translations/:locale" => "themes#get_translations"
put "setting" => "themes#update_single_setting"
get "objects_setting_metadata/:setting_name" => "themes#objects_setting_metadata"
end
collection do
post "import" => "themes#import"
post "upload_asset" => "themes#upload_asset"
post "generate_key_pair" => "themes#generate_key_pair"
delete "bulk_destroy" => "themes#bulk_destroy"
end
end
2020-06-05 10:49:31 +08:00
scope "/customize", constraints: AdminConstraint.new do
resources :user_fields,
only: %i[index create update destroy],
constraints: AdminConstraint.new
resources :emojis, only: %i[index create destroy], constraints: AdminConstraint.new
resources :form_templates, constraints: AdminConstraint.new, path: "/form-templates" do
collection { get "preview" => "form_templates#preview" }
end
2020-06-05 10:49:31 +08:00
get "themes/:id/:target/:field_name/edit" => "themes#index"
get "themes/:id" => "themes#index"
get "components/:id" => "themes#index"
get "components/:id/:target/:field_name/edit" => "themes#index"
get "themes/:id/export" => "themes#export"
get "themes/:id/schema/:setting_name" => "themes#schema"
get "components/:id/schema/:setting_name" => "themes#schema"
2020-06-05 10:49:31 +08:00
# They have periods in their URLs often:
get "site_texts" => "site_texts#index"
get "site_texts/:id.json" => "site_texts#show", :constraints => { id: /[\w.\-\+\%\&]+/i }
get "site_texts/:id" => "site_texts#show", :constraints => { id: /[\w.\-\+\%\&]+/i }
put "site_texts/:id.json" => "site_texts#update", :constraints => { id: /[\w.\-\+\%\&]+/i }
put "site_texts/:id" => "site_texts#update", :constraints => { id: /[\w.\-\+\%\&]+/i }
delete "site_texts/:id.json" => "site_texts#revert",
:constraints => {
id: /[\w.\-\+\%\&]+/i,
}
delete "site_texts/:id" => "site_texts#revert", :constraints => { id: /[\w.\-\+\%\&]+/i }
put "site_texts/:id/dismiss_outdated" => "site_texts#dismiss_outdated",
:constraints => {
id: /[\w.\-\+\%\&]+/i,
}
put "site_texts/:id/dismiss_outdated.json" => "site_texts#dismiss_outdated",
:constraints => {
id: /[\w.\-\+\%\&]+/i,
}
get "reseed" => "site_texts#get_reseed_options"
post "reseed" => "site_texts#reseed"
2016-04-07 03:57:54 +08:00
get "email_templates" => "email_templates#index"
get "email_templates/(:id)" => "email_templates#show", :constraints => { id: /[0-9a-z_.]+/ }
put "email_templates/(:id)" => "email_templates#update",
:constraints => {
id: /[0-9a-z_.]+/,
}
delete "email_templates/(:id)" => "email_templates#revert",
:constraints => {
id: /[0-9a-z_.]+/,
}
get "robots" => "robots_txt#show"
put "robots.json" => "robots_txt#update"
delete "robots.json" => "robots_txt#reset"
resource :email_style, only: %i[show update]
get "email_style/:field" => "email_styles#show", :constraints => { field: /html|css/ }
end
resources :embeddable_hosts, only: %i[create update destroy], constraints: AdminConstraint.new
resources :color_schemes,
only: %i[index create update destroy],
constraints: AdminConstraint.new
resources :permalinks, only: %i[index create destroy], constraints: AdminConstraint.new
scope "/customize" do
resources :watched_words, only: %i[index create destroy] do
collection do
get "action/:id" => "watched_words#index"
get "action/:id/download" => "watched_words#download"
delete "action/:id" => "watched_words#clear_all"
end
end
post "watched_words/upload" => "watched_words#upload"
end
2013-12-24 07:50:36 +08:00
get "version_check" => "versions#show"
2014-02-13 12:33:40 +08:00
2019-04-01 18:39:49 +08:00
get "dashboard" => "dashboard#index"
get "dashboard/general" => "dashboard#general"
get "dashboard/moderation" => "dashboard#moderation"
get "dashboard/security" => "dashboard#security"
get "dashboard/reports" => "dashboard#reports"
get "dashboard/whats-new" => "dashboard#new_features"
get "/whats-new" => "dashboard#new_features"
resources :dashboard, only: [:index] do
2013-12-24 07:50:36 +08:00
collection { get "problems" }
end
2014-02-13 12:33:40 +08:00
resources :api, only: [:index], constraints: AdminConstraint.new do
2013-03-26 09:04:28 +08:00
collection do
resources :keys, controller: "api", only: %i[index show update create destroy] do
collection { get "scopes" => "api#scopes" }
member do
post "revoke" => "api#revoke_key"
post "undo-revoke" => "api#undo_revoke_key"
2020-06-05 10:49:31 +08:00
end
end
resources :web_hooks, only: %i[index create show edit update destroy]
get "web_hook_events/:id" => "web_hooks#list_events", :as => :web_hook_events
get "web_hooks/:id/events/bulk" => "web_hooks#bulk_events"
post "web_hooks/:web_hook_id/events/:event_id/redeliver" => "web_hooks#redeliver_event"
post "web_hooks/:id/ping" => "web_hooks#ping"
2020-06-05 10:49:31 +08:00
end
2013-03-26 09:04:28 +08:00
end
2014-02-13 12:33:40 +08:00
resources :backups, only: %i[index create], constraints: AdminConstraint.new do
member do
get "" => "backups#show", :constraints => { id: RouteFormat.backup }
put "" => "backups#email", :constraints => { id: RouteFormat.backup }
delete "" => "backups#destroy", :constraints => { id: RouteFormat.backup }
post "restore" => "backups#restore", :constraints => { id: RouteFormat.backup }
2014-02-13 12:33:40 +08:00
end
collection do
# multipart uploads
post "create-multipart" => "backups#create_multipart", :format => :json
post "complete-multipart" => "backups#complete_multipart", :format => :json
post "abort-multipart" => "backups#abort_multipart", :format => :json
post "batch-presign-multipart-parts" => "backups#batch_presign_multipart_parts",
:format => :json
2014-02-13 12:33:40 +08:00
get "logs" => "backups#logs"
get "status" => "backups#status"
delete "cancel" => "backups#cancel"
post "rollback" => "backups#rollback"
2014-02-13 12:33:40 +08:00
put "readonly" => "backups#readonly"
2014-05-28 04:14:37 +08:00
get "upload" => "backups#check_backup_chunk"
post "upload" => "backups#upload_backup_chunk"
get "upload_url" => "backups#create_upload_url"
2020-06-05 10:49:31 +08:00
end
2014-02-13 12:33:40 +08:00
end
2014-02-15 07:50:08 +08:00
resources :badges,
only: %i[index new show create update destroy],
constraints: AdminConstraint.new do
2014-03-05 20:52:20 +08:00
collection do
get "/award/:badge_id" => "badges#award"
post "/award/:badge_id" => "badges#mass_award"
2014-03-05 20:52:20 +08:00
get "types" => "badges#badge_types"
2014-07-27 16:22:01 +08:00
post "badge_groupings" => "badges#save_badge_groupings"
post "preview" => "badges#preview"
2020-06-05 10:49:31 +08:00
end
2014-03-05 20:52:20 +08:00
end
namespace :config, constraints: StaffConstraint.new do
resources :flags, only: %i[index] do
put "toggle"
put "reorder/:direction" => "flags#reorder"
end
resources :about, constraints: AdminConstraint.new, only: %i[index] do
collection { put "/" => "about#update" }
end
end
end # admin namespace
2013-02-06 03:16:51 +08:00
2013-12-24 07:50:36 +08:00
get "email/unsubscribe/:key" => "email#unsubscribe", :as => "email_unsubscribe"
get "email/unsubscribed" => "email#unsubscribed", :as => "email_unsubscribed"
post "email/unsubscribe/:key" => "email#perform_unsubscribe", :as => "email_perform_unsubscribe"
2013-02-06 03:16:51 +08:00
2016-08-26 04:33:29 +08:00
get "extra-locales/:bundle" => "extra_locales#show"
resources :session, id: RouteFormat.username, only: %i[create destroy become] do
2018-03-28 11:31:43 +08:00
get "become" if !Rails.env.production?
2013-12-24 07:50:36 +08:00
collection { post "forgot_password" }
2013-02-06 03:16:51 +08:00
end
get "review" => "reviewables#index" # For ember app
get "review/:reviewable_id" => "reviewables#show", :constraints => { reviewable_id: /\d+/ }
get "review/:reviewable_id/explain" => "reviewables#explain",
:constraints => {
reviewable_id: /\d+/,
}
get "review/count" => "reviewables#count"
get "review/topics" => "reviewables#topics"
get "review/settings" => "reviewables#settings"
get "review/user-menu-list" => "reviewables#user_menu_list", :format => :json
put "review/settings" => "reviewables#settings"
put "review/:reviewable_id/perform/:action_id" => "reviewables#perform",
:constraints => {
reviewable_id: /\d+/,
action_id: /[a-z\_]+/,
}
put "review/:reviewable_id" => "reviewables#update", :constraints => { reviewable_id: /\d+/ }
delete "review/:reviewable_id" => "reviewables#destroy",
:constraints => {
reviewable_id: /\d+/,
}
2020-06-05 10:49:31 +08:00
resources :reviewable_claimed_topics, only: %i[create destroy]
2020-06-05 10:49:31 +08:00
get "session/sso" => "session#sso"
get "session/sso_login" => "session#sso_login"
get "session/sso_provider" => "session#sso_provider"
get "session/current" => "session#current"
2013-12-24 07:50:36 +08:00
get "session/csrf" => "session#csrf"
get "session/hp" => "session#get_honeypot_value"
get "session/email-login/:token" => "session#email_login_info"
post "session/email-login/:token" => "session#email_login"
get "session/otp/:token" => "session#one_time_password", :constraints => { token: /[0-9a-f]+/ }
post "session/otp/:token" => "session#one_time_password", :constraints => { token: /[0-9a-f]+/ }
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
get "session/2fa" => "session#second_factor_auth_show"
post "session/2fa" => "session#second_factor_auth_perform"
if Rails.env.test?
post "session/2fa/test-action" => "session#test_second_factor_restricted_route"
end
DEV: Add routes and controller actions for passkeys (2/3) (#23587) This is part 2 (of 3) for passkeys support. This adds a hidden site setting plus routes and controller actions. 1. registering passkeys Passkeys are registered in a two-step process. First, `create_passkey` returns details for the browser to create a passkey. This includes - a challenge - the relying party ID and Origin - the user's secure identifier - the supported algorithms - the user's existing passkeys (if any) Then the browser creates a key with this information, and submits it to the server via `register_passkey`. 2. authenticating passkeys A similar process happens here as well. First, a challenge is created and sent to the browser. Then the browser makes a public key credential and submits it to the server via `passkey_auth_perform`. 3. renaming/deleting passkeys These routes allow changing the name of a key and deleting it. 4. checking if session is trusted for sensitive actions Since a passkey is a password replacement, we want to make sure to confirm the user's identity before allowing adding/deleting passkeys. The u/trusted-session GET route returns success if user has confirmed their session (and failed if user hasn't). In the frontend (in the next PR), we're using these routes to show the password confirmation screen. The `/u/confirm-session` route allows the user to confirm their session with a password. The latter route's functionality already existed in core, under the 2FA flow, but it has been abstracted into its own here so it can be used independently. Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
2023-10-12 02:36:54 +08:00
get "session/passkey/challenge" => "session#passkey_challenge"
post "session/passkey/auth" => "session#passkey_login"
get "session/scopes" => "session#scopes"
get "composer/mentions" => "composer#mentions"
get "composer_messages" => "composer_messages#index"
get "composer_messages/user_not_seen_in_a_while" => "composer_messages#user_not_seen_in_a_while"
2020-06-05 10:49:31 +08:00
post "login" => "static#enter"
get "login" => "static#show", :id => "login"
get "login-preferences" => "static#show", :id => "login"
get "signup" => "static#show", :id => "signup"
get "password-reset" => "static#show", :id => "password_reset"
get "privacy" => "static#show", :id => "privacy", :as => "privacy"
get "tos" => "static#show", :id => "tos", :as => "tos"
get "faq" => "static#show", :id => "faq"
%w[guidelines rules conduct].each do |guidelines_alias|
get guidelines_alias => "static#show", :id => "guidelines", :as => guidelines_alias
end
get "my/*path", to: "users#my_redirect"
get ".well-known/change-password",
to: redirect(relative_url_root + "my/preferences/security", status: 302)
get "user-cards" => "users#cards", :format => :json
get "directory-columns" => "directory_columns#index", :format => :json
get "edit-directory-columns" => "edit_directory_columns#index", :format => :json
put "edit-directory-columns" => "edit_directory_columns#update", :format => :json
2017-03-31 10:04:00 +08:00
%w[users u].each_with_index do |root_path, index|
get "#{root_path}" => "users#index", :constraints => { format: "html" }
resources :users, only: %i[create], path: root_path do
2017-03-31 10:04:00 +08:00
collection do
get "check_username"
get "check_email"
2020-06-05 10:49:31 +08:00
end
2017-03-31 10:04:00 +08:00
end
2020-06-05 10:49:31 +08:00
DEV: Add routes and controller actions for passkeys (2/3) (#23587) This is part 2 (of 3) for passkeys support. This adds a hidden site setting plus routes and controller actions. 1. registering passkeys Passkeys are registered in a two-step process. First, `create_passkey` returns details for the browser to create a passkey. This includes - a challenge - the relying party ID and Origin - the user's secure identifier - the supported algorithms - the user's existing passkeys (if any) Then the browser creates a key with this information, and submits it to the server via `register_passkey`. 2. authenticating passkeys A similar process happens here as well. First, a challenge is created and sent to the browser. Then the browser makes a public key credential and submits it to the server via `passkey_auth_perform`. 3. renaming/deleting passkeys These routes allow changing the name of a key and deleting it. 4. checking if session is trusted for sensitive actions Since a passkey is a password replacement, we want to make sure to confirm the user's identity before allowing adding/deleting passkeys. The u/trusted-session GET route returns success if user has confirmed their session (and failed if user hasn't). In the frontend (in the next PR), we're using these routes to show the password confirmation screen. The `/u/confirm-session` route allows the user to confirm their session with a password. The latter route's functionality already existed in core, under the 2FA flow, but it has been abstracted into its own here so it can be used independently. Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
2023-10-12 02:36:54 +08:00
get "#{root_path}/trusted-session" => "users#trusted_session"
post "#{root_path}/confirm-session" => "users#confirm_session"
post "#{root_path}/second_factors" => "users#list_second_factors"
put "#{root_path}/second_factor" => "users#update_second_factor"
2020-06-05 10:49:31 +08:00
post "#{root_path}/create_second_factor_security_key" =>
"users#create_second_factor_security_key"
post "#{root_path}/register_second_factor_security_key" =>
"users#register_second_factor_security_key"
put "#{root_path}/security_key" => "users#update_security_key"
post "#{root_path}/create_second_factor_totp" => "users#create_second_factor_totp"
post "#{root_path}/enable_second_factor_totp" => "users#enable_second_factor_totp"
put "#{root_path}/disable_second_factor" => "users#disable_second_factor"
2020-06-05 10:49:31 +08:00
2018-06-28 16:12:32 +08:00
put "#{root_path}/second_factors_backup" => "users#create_second_factor_backup"
2020-06-05 10:49:31 +08:00
DEV: Add routes and controller actions for passkeys (2/3) (#23587) This is part 2 (of 3) for passkeys support. This adds a hidden site setting plus routes and controller actions. 1. registering passkeys Passkeys are registered in a two-step process. First, `create_passkey` returns details for the browser to create a passkey. This includes - a challenge - the relying party ID and Origin - the user's secure identifier - the supported algorithms - the user's existing passkeys (if any) Then the browser creates a key with this information, and submits it to the server via `register_passkey`. 2. authenticating passkeys A similar process happens here as well. First, a challenge is created and sent to the browser. Then the browser makes a public key credential and submits it to the server via `passkey_auth_perform`. 3. renaming/deleting passkeys These routes allow changing the name of a key and deleting it. 4. checking if session is trusted for sensitive actions Since a passkey is a password replacement, we want to make sure to confirm the user's identity before allowing adding/deleting passkeys. The u/trusted-session GET route returns success if user has confirmed their session (and failed if user hasn't). In the frontend (in the next PR), we're using these routes to show the password confirmation screen. The `/u/confirm-session` route allows the user to confirm their session with a password. The latter route's functionality already existed in core, under the 2FA flow, but it has been abstracted into its own here so it can be used independently. Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
2023-10-12 02:36:54 +08:00
post "#{root_path}/create_passkey" => "users#create_passkey"
post "#{root_path}/register_passkey" => "users#register_passkey"
put "#{root_path}/rename_passkey/:id" => "users#rename_passkey"
delete "#{root_path}/delete_passkey/:id" => "users#delete_passkey"
put "#{root_path}/update-activation-email" => "users#update_activation_email"
post "#{root_path}/email-login" => "users#email_login"
2017-03-31 10:04:00 +08:00
get "#{root_path}/admin-login" => "users#admin_login"
put "#{root_path}/admin-login" => "users#admin_login"
post "#{root_path}/toggle-anon" => "users#toggle_anon"
post "#{root_path}/read-faq" => "users#read_faq"
get "#{root_path}/recent-searches" => "users#recent_searches",
:constraints => {
format: "json",
}
delete "#{root_path}/recent-searches" => "users#reset_recent_searches",
:constraints => {
format: "json",
}
2017-03-31 10:04:00 +08:00
get "#{root_path}/search/users" => "users#search_users"
2020-06-05 10:49:31 +08:00
get(
{ "#{root_path}/account-created/" => "users#account_created" }.merge(
index == 1 ? { as: :users_account_created } : { as: :old_account_created },
),
)
2020-06-05 10:49:31 +08:00
get "#{root_path}/account-created/resent" => "users#account_created"
get "#{root_path}/account-created/edit-email" => "users#account_created"
2020-01-15 18:27:12 +08:00
get(
{ "#{root_path}/password-reset/:token" => "users#password_reset_show" }.merge(
index == 1 ? { as: :password_reset_token } : {},
),
2020-01-15 18:27:12 +08:00
)
2017-03-31 10:04:00 +08:00
get "#{root_path}/confirm-email-token/:token" => "users#confirm_email_token",
:constraints => {
format: "json",
}
2020-01-15 18:27:12 +08:00
put "#{root_path}/password-reset/:token" => "users#password_reset_update"
2017-03-31 10:04:00 +08:00
get "#{root_path}/activate-account/:token" => "users#activate_account"
put(
{ "#{root_path}/activate-account/:token" => "users#perform_account_activation" }.merge(
index == 1 ? { as: "perform_activate_account" } : {},
),
2017-03-31 10:04:00 +08:00
)
2020-06-05 10:49:31 +08:00
get "#{root_path}/confirm-old-email/:token" => "users_email#show_confirm_old_email"
put "#{root_path}/confirm-old-email/:token" => "users_email#confirm_old_email"
2020-06-05 10:49:31 +08:00
get "#{root_path}/confirm-new-email/:token" => "users_email#show_confirm_new_email"
put "#{root_path}/confirm-new-email/:token" => "users_email#confirm_new_email"
2020-06-05 10:49:31 +08:00
get(
{
"#{root_path}/confirm-admin/:token" => "users#confirm_admin",
:constraints => {
token: /[0-9a-f]+/,
},
}.merge(index == 1 ? { as: "confirm_admin" } : {}),
)
post "#{root_path}/confirm-admin/:token" => "users#confirm_admin",
:constraints => {
token: /[0-9a-f]+/,
}
get "#{root_path}/:username/private-messages" => "users#show",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/private-messages/:filter" => "users#show",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/messages" => "users#show",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/messages/:filter" => "users#show",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/messages/group/:group_name" => "users#show",
:constraints => {
username: RouteFormat.username,
group_name: RouteFormat.username,
}
get "#{root_path}/:username/messages/group/:group_name/:filter" => "users#show",
:constraints => {
username: RouteFormat.username,
group_name: RouteFormat.username,
}
get "#{root_path}/:username/messages/tags/:tag_id" => "list#private_messages_tag",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username.json" => "users#show",
:constraints => {
username: RouteFormat.username,
},
:defaults => {
format: :json,
}
get(
{
"#{root_path}/:username" => "users#show",
:constraints => {
username: RouteFormat.username,
},
}.merge(index == 1 ? { as: "user" } : {}),
)
put "#{root_path}/:username" => "users#update",
:constraints => {
username: RouteFormat.username,
},
:defaults => {
format: :json,
}
get "#{root_path}/:username/emails" => "users#check_emails",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/sso-email" => "users#check_sso_email",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/sso-payload" => "users#check_sso_payload",
2020-06-06 00:42:12 +08:00
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/email" => "users_email#index",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/account" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/security" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/profile" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/emails" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
put "#{root_path}/:username/preferences/primary-email" => "users#update_primary_email",
:format => :json,
:constraints => {
username: RouteFormat.username,
}
delete "#{root_path}/:username/preferences/email" => "users#destroy_email",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/notifications" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/tracking" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/users" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/tags" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/interface" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/navigation-menu" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/apps" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
post "#{root_path}/:username/preferences/email" => "users_email#create",
:constraints => {
username: RouteFormat.username,
}
put "#{root_path}/:username/preferences/email" => "users_email#update",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/badge_title" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
put "#{root_path}/:username/preferences/badge_title" => "users#badge_title",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/username" => "users#preferences",
:constraints => {
username: RouteFormat.username,
}
put "#{root_path}/:username/preferences/username" => "users#username",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/preferences/second-factor" => "users#preferences",
2018-06-28 16:12:32 +08:00
:constraints => {
username: RouteFormat.username,
}
delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image",
:constraints => {
username: RouteFormat.username,
}
put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar",
2018-07-18 18:57:43 +08:00
:constraints => {
username: RouteFormat.username,
}
put "#{root_path}/:username/preferences/avatar/select" => "users#select_avatar",
:constraints => {
username: RouteFormat.username,
}
post "#{root_path}/:username/preferences/revoke-account" => "users#revoke_account",
:constraints => {
username: RouteFormat.username,
}
post "#{root_path}/:username/preferences/revoke-auth-token" => "users#revoke_auth_token",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/staff-info" => "users#staff_info",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/summary" => "users#summary",
:constraints => {
username: RouteFormat.username,
}
put "#{root_path}/:username/notification_level" => "users#notification_level",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/invited" => "users#invited",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/invited/:filter" => "users#invited",
:constraints => {
username: RouteFormat.username,
}
2017-03-31 10:04:00 +08:00
post "#{root_path}/action/send_activation_email" => "users#send_activation_email"
get "#{root_path}/:username/summary" => "users#show",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/activity/topics.rss" => "list#user_topics_feed",
:format => :rss,
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/activity.rss" => "posts#user_posts_feed",
:format => :rss,
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/activity.json" => "posts#user_posts_feed",
:format => :json,
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/activity" => "users#show",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/activity/:filter" => "users#show",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/badges" => "users#badges",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/bookmarks" => "users#bookmarks",
:constraints => {
username: RouteFormat.username,
format: /(json|ics)/,
}
get "#{root_path}/:username/user-menu-bookmarks" => "users#user_menu_bookmarks",
:constraints => {
username: RouteFormat.username,
format: :json,
}
get "#{root_path}/:username/user-menu-private-messages" => "users#user_menu_messages",
:constraints => {
username: RouteFormat.username,
format: :json,
}
get "#{root_path}/:username/notifications" => "users#show",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/notifications/:filter" => "users#show",
:constraints => {
username: RouteFormat.username,
}
delete "#{root_path}/:username" => "users#destroy",
2017-03-31 10:04:00 +08:00
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/by-external/:external_id" => "users#show",
:constraints => {
external_id: %r{[^/]+},
}
get "#{root_path}/by-external/:external_provider/:external_id" => "users#show",
:constraints => {
external_id: %r{[^/]+},
}
get "#{root_path}/:username/deleted-posts" => "users#show",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/private-message-topic-tracking-state" =>
"users#private_message_topic_tracking_state",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/profile-hidden" => "users#profile_hidden"
put "#{root_path}/:username/feature-topic" => "users#feature_topic",
:constraints => {
username: RouteFormat.username,
}
put "#{root_path}/:username/clear-featured-topic" => "users#clear_featured_topic",
:constraints => {
username: RouteFormat.username,
}
get "#{root_path}/:username/card.json" => "users#show_card",
:format => :json,
:constraints => {
username: RouteFormat.username,
}
2017-03-31 10:04:00 +08:00
end
get "user-badges/:username.json" => "user_badges#username",
:constraints => {
username: RouteFormat.username,
},
:defaults => {
format: :json,
}
get "user-badges/:username" => "user_badges#username",
:constraints => {
username: RouteFormat.username,
}
post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar",
:constraints => {
username: RouteFormat.username,
}
get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter",
:constraints => {
hostname: /[\w\.-]+/,
size: /\d+/,
username: RouteFormat.username,
format: :png,
}
get "user_avatar/:hostname/:username/:size/:version.png" => "user_avatars#show",
:constraints => {
hostname: /[\w\.-]+/,
size: /\d+/,
username: RouteFormat.username,
format: :png,
}
get "letter_avatar_proxy/:version/letter/:letter/:color/:size.png" =>
"user_avatars#show_proxy_letter",
:constraints => {
format: :png,
}
get "svg-sprite/:hostname/svg-:theme_id-:version.js" => "svg_sprite#show",
:constraints => {
hostname: /[\w\.-]+/,
version: /\h{40}/,
theme_id: /([0-9]+)?/,
format: :js,
}
get "svg-sprite/search/:keyword" => "svg_sprite#search",
:format => false,
:constraints => {
keyword: /[-a-z0-9\s\%]+/,
}
get "svg-sprite/picker-search" => "svg_sprite#icon_picker_search",
:defaults => {
format: :json,
}
get "svg-sprite/:hostname/icon(/:color)/:name.svg" => "svg_sprite#svg_icon",
:constraints => {
hostname: /[\w\.-]+/,
name: /[-a-z0-9\s\%]+/,
color: /(\h{3}{1,2})/,
format: :svg,
}
get "highlight-js/:hostname/:version.js" => "highlight_js#show",
:constraints => {
hostname: /[\w\.-]+/,
format: :js,
}
get "stylesheets/:name" => "stylesheets#show_source_map",
:constraints => {
name: /[-a-z0-9_]+/,
format: /css\.map/,
},
:format => true
get "stylesheets/:name" => "stylesheets#show",
:constraints => {
name: /[-a-z0-9_]+/,
format: "css",
},
:format => true
get "color-scheme-stylesheet/:id(/:theme_id)" => "stylesheets#color_scheme",
:constraints => {
format: :json,
}
get "theme-javascripts/:digest" => "theme_javascripts#show",
:constraints => {
digest: /\h{40}/,
format: :js,
},
:format => true
get "theme-javascripts/:digest" => "theme_javascripts#show_map",
:constraints => {
digest: /\h{40}/,
format: :map,
},
:format => true
get "theme-javascripts/tests/:theme_id-:digest.js" => "theme_javascripts#show_tests"
post "uploads/lookup-metadata" => "uploads#metadata"
post "uploads" => "uploads#create"
post "uploads/lookup-urls" => "uploads#lookup_urls"
FEATURE: Uppy direct S3 multipart uploads in composer (#14051) This pull request introduces the endpoints required, and the JavaScript functionality in the `ComposerUppyUpload` mixin, for direct S3 multipart uploads. There are four new endpoints in the uploads controller: * `create-multipart.json` - Creates the multipart upload in S3 along with an `ExternalUploadStub` record, storing information about the file in the same way as `generate-presigned-put.json` does for regular direct S3 uploads * `batch-presign-multipart-parts.json` - Takes a list of part numbers and the unique identifier for an `ExternalUploadStub` record, and generates the presigned URLs for those parts if the multipart upload still exists and if the user has permission to access that upload * `complete-multipart.json` - Completes the multipart upload in S3. Needs the full list of part numbers and their associated ETags which are returned when the part is uploaded to the presigned URL above. Only works if the user has permission to access the associated `ExternalUploadStub` record and the multipart upload still exists. After we confirm the upload is complete in S3, we go through the regular `UploadCreator` flow, the same as `complete-external-upload.json`, and promote the temporary upload S3 into a full `Upload` record, moving it to its final destination. * `abort-multipart.json` - Aborts the multipart upload on S3 and destroys the `ExternalUploadStub` record if the user has permission to access that upload. Also added are a few new columns to `ExternalUploadStub`: * multipart - Whether or not this is a multipart upload * external_upload_identifier - The "upload ID" for an S3 multipart upload * filesize - The size of the file when the `create-multipart.json` or `generate-presigned-put.json` is called. This is used for validation. When the user completes a direct S3 upload, either regular or multipart, we take the `filesize` that was captured when the `ExternalUploadStub` was first created and compare it with the final `Content-Length` size of the file where it is stored in S3. Then, if the two do not match, we throw an error, delete the file on S3, and ban the user from uploading files for N (default 5) minutes. This would only happen if the user uploads a different file than what they first specified, or in the case of multipart uploads uploaded larger chunks than needed. This is done to prevent abuse of S3 storage by bad actors. Also included in this PR is an update to vendor/uppy.js. This has been built locally from the latest uppy source at https://github.com/transloadit/uppy/commit/d613b849a6591083f8a0968aa8d66537e231bbcd. This must be done so that I can get my multipart upload changes into Discourse. When the Uppy team cuts a proper release, we can bump the package.json versions instead.
2021-08-25 06:46:54 +08:00
# direct to s3 uploads
post "uploads/generate-presigned-put" => "uploads#generate_presigned_put", :format => :json
post "uploads/complete-external-upload" => "uploads#complete_external_upload", :format => :json
# multipart uploads
post "uploads/create-multipart" => "uploads#create_multipart", :format => :json
post "uploads/complete-multipart" => "uploads#complete_multipart", :format => :json
post "uploads/abort-multipart" => "uploads#abort_multipart", :format => :json
post "uploads/batch-presign-multipart-parts" => "uploads#batch_presign_multipart_parts",
:format => :json
FEATURE: Initial implementation of direct S3 uploads with uppy and stubs (#13787) This adds a few different things to allow for direct S3 uploads using uppy. **These changes are still not the default.** There are hidden `enable_experimental_image_uploader` and `enable_direct_s3_uploads` settings that must be turned on for any of this code to be used, and even if they are turned on only the User Card Background for the user profile actually uses uppy-image-uploader. A new `ExternalUploadStub` model and database table is introduced in this pull request. This is used to keep track of uploads that are uploaded to a temporary location in S3 with the direct to S3 code, and they are eventually deleted a) when the direct upload is completed and b) after a certain time period of not being used. ### Starting a direct S3 upload When an S3 direct upload is initiated with uppy, we first request a presigned PUT URL from the new `generate-presigned-put` endpoint in `UploadsController`. This generates an S3 key in the `temp` folder inside the correct bucket path, along with any metadata from the clientside (e.g. the SHA1 checksum described below). This will also create an `ExternalUploadStub` and store the details of the temp object key and the file being uploaded. Once the clientside has this URL, uppy will upload the file direct to S3 using the presigned URL. Once the upload is complete we go to the next stage. ### Completing a direct S3 upload Once the upload to S3 is done we call the new `complete-external-upload` route with the unique identifier of the `ExternalUploadStub` created earlier. Only the user who made the stub can complete the external upload. One of two paths is followed via the `ExternalUploadManager`. 1. If the object in S3 is too large (currently 100mb defined by `ExternalUploadManager::DOWNLOAD_LIMIT`) we do not download and generate the SHA1 for that file. Instead we create the `Upload` record via `UploadCreator` and simply copy it to its final destination on S3 then delete the initial temp file. Several modifications to `UploadCreator` have been made to accommodate this. 2. If the object in S3 is small enough, we download it. When the temporary S3 file is downloaded, we compare the SHA1 checksum generated by the browser with the actual SHA1 checksum of the file generated by ruby. The browser SHA1 checksum is stored on the object in S3 with metadata, and is generated via the `UppyChecksum` plugin. Keep in mind that some browsers will not generate this due to compatibility or other issues. We then follow the normal `UploadCreator` path with one exception. To cut down on having to re-upload the file again, if there are no changes (such as resizing etc) to the file in `UploadCreator` we follow the same copy + delete temp path that we do for files that are too large. 3. Finally we return the serialized upload record back to the client There are several errors that could happen that are handled by `UploadsController` as well. Also in this PR is some refactoring of `displayErrorForUpload` to handle both uppy and jquery file uploader errors.
2021-07-28 06:42:25 +08:00
# used to download original images
get "uploads/:site/:sha(.:extension)" => "uploads#show",
:constraints => {
site: /\w+/,
sha: /\h{40}/,
extension: /[a-z0-9\._]+/i,
}
get "uploads/short-url/:base62(.:extension)" => "uploads#show_short",
:constraints => {
site: /\w+/,
base62: /[a-zA-Z0-9]+/,
extension: /[a-zA-Z0-9\._-]+/i,
},
:as => :upload_short
# used to download attachments
get "uploads/:site/original/:tree:sha(.:extension)" => "uploads#show",
:constraints => {
site: /\w+/,
tree: %r{([a-z0-9]+/)+}i,
sha: /\h{40}/,
extension: /[a-z0-9\._]+/i,
}
if Rails.env.test?
get "uploads/:site/test_:index/original/:tree:sha(.:extension)" => "uploads#show",
:constraints => {
site: /\w+/,
index: /\d+/,
tree: %r{([a-z0-9]+/)+}i,
sha: /\h{40}/,
extension: /[a-z0-9\._]+/i,
}
2020-06-05 10:49:31 +08:00
end
2015-05-20 20:55:42 +08:00
# used to download attachments (old route)
get "uploads/:site/:id/:sha" => "uploads#show",
:constraints => {
site: /\w+/,
id: /\d+/,
sha: /\h{16}/,
format: /.*/,
}
# NOTE: secure-media-uploads is the old form, all new URLs generated for
# secure uploads will be secure-uploads, this is left in for backwards
# compat without needing to rebake all posts for each site.
get "secure-media-uploads/*path(.:extension)" => "uploads#_show_secure_deprecated",
:constraints => {
extension: /[a-z0-9\._]+/i,
}
get "secure-uploads/*path(.:extension)" => "uploads#show_secure",
:constraints => {
extension: /[a-z0-9\._]+/i,
}
get "posts" => "posts#latest", :id => "latest_posts", :constraints => { format: /(json|rss)/ }
get "private-posts" => "posts#latest",
:id => "private_posts",
:constraints => {
format: /(json|rss)/,
}
2013-12-24 07:50:36 +08:00
get "posts/by_number/:topic_id/:post_number" => "posts#by_number"
get "posts/by-date/:topic_id/:date" => "posts#by_date"
2013-12-24 07:50:36 +08:00
get "posts/:id/reply-history" => "posts#reply_history"
get "posts/:id/reply-ids" => "posts#reply_ids"
get "posts/:username/deleted" => "posts#deleted_posts",
:constraints => {
username: RouteFormat.username,
}
get "posts/:username/pending" => "posts#pending",
:constraints => {
username: RouteFormat.username,
}
%w[groups g].each do |root_path|
resources :groups,
only: %i[index show new edit update],
id: RouteFormat.username,
path: root_path do
get "posts.rss" => "groups#posts_feed", :format => :rss
get "mentions.rss" => "groups#mentions_feed", :format => :rss
get "members"
get "posts"
get "mentions"
get "mentionable"
get "messageable"
get "logs" => "groups#histories"
FEATURE: Improve group email settings UI (#13083) This overhauls the user interface for the group email settings management, aiming to make it a lot easier to test the settings entered and confirm they are correct before proceeding. We do this by forcing the user to test the settings before they can be saved to the database. It also includes some quality of life improvements around setting up IMAP and SMTP for our first supported provider, GMail. This PR does not remove the old group email config, that will come in a subsequent PR. This is related to https://meta.discourse.org/t/imap-support-for-group-inboxes/160588 so read that if you would like more backstory. ### UI Both site settings of `enable_imap` and `enable_smtp` must be true to test this. You must enable SMTP first to enable IMAP. You can prefill the SMTP settings with GMail configuration. To proceed with saving these settings you must test them, which is handled by the EmailSettingsValidator. If there is an issue with the configuration or credentials a meaningful error message should be shown. IMAP settings must also be validated when IMAP is enabled, before saving. When saving IMAP, we fetch the mailboxes for that account and populate them. This mailbox must be selected and saved for IMAP to work (the feature acts as though it is disabled until the mailbox is selected and saved): ### Database & Backend This adds several columns to the Groups table. The purpose of this change is to make it much more explicit that SMTP/IMAP is enabled for a group, rather than relying on settings not being null. Also included is an UPDATE query to backfill these columns. These columns are automatically filled when updating the group. For GMail, we now filter the mailboxes returned. This is so users cannot use a mailbox like Sent or Trash for syncing, which would generally be disastrous. There is a new group endpoint for testing email settings. This may be useful in the future for other places in our UI, at which point it can be extracted to a more generic endpoint or module to be included.
2021-05-28 07:28:18 +08:00
post "test_email_settings"
2013-12-24 07:50:36 +08:00
collection do
get "check-name" => "groups#check_name"
get "custom/new" => "groups#new", :constraints => StaffConstraint.new
get "search" => "groups#search"
end
member do
2020-06-05 10:49:31 +08:00
%w[
activity
activity/:filter
2020-06-05 10:49:31 +08:00
requests
messages
messages/inbox
messages/archive
manage
manage/profile
manage/members
manage/membership
manage/interaction
manage/email
manage/categories
manage/tags
manage/logs
].each { |path| get path => "groups#show" }
get "permissions" => "groups#permissions"
put "members" => "groups#add_members"
put "owners" => "groups#add_owners"
put "join" => "groups#join"
delete "members" => "groups#remove_member"
delete "leave" => "groups#leave"
post "request_membership" => "groups#request_membership"
put "handle_membership_request" => "groups#handle_membership_request"
post "notifications" => "groups#set_notifications"
end
end
end
resources :associated_groups, only: %i[index], constraints: AdminConstraint.new
post "slugs", to: "slugs#generate"
# aliases so old API code works
delete "admin/groups/:id/members" => "groups#remove_member", :constraints => AdminConstraint.new
put "admin/groups/:id/members" => "groups#add_members", :constraints => AdminConstraint.new
2020-06-05 10:49:31 +08:00
put "bookmarks/bulk"
resources :posts, only: %i[show update create destroy] do
Improving bookmarks part 1 (#8466) Note: All of this functionality is hidden behind a hidden, default false, site setting called `enable_bookmarks_with_reminders`. Also, any feedback on Ember code would be greatly appreciated! This is part 1 of the bookmark improvements. The next PR will address the backend logic to send reminder notifications for bookmarked posts to users. This PR adds the following functionality: * We are adding a new `bookmarks` table and `Bookmark` model to make the bookmarks a first-class citizen and to allow attaching reminders to them. * Posts now have a new button in their actions menu that has the icon of an actual book * Clicking the button opens the new bookmark modal. * Both name and the reminder type are optional. * If you close the modal without doing anything, the bookmark is saved with no reminder. * If you click the Cancel button, no bookmark is saved at all. * All of the reminder type tiles are dynamic and the times they show will be based on your user timezone set in your profile (this should already be set for you). * If for some reason a user does not have their timezone set they will not be able to set a reminder, but they will still be able to create a bookmark. * A bookmark can be deleted by clicking on the book icon again which will be red if the post is bookmarked. This PR does NOT do anything to migrate or change existing bookmarks in the form of `PostActions`, the two features live side-by-side here. Also this does nothing to the topic bookmarking.
2019-12-11 12:04:02 +08:00
delete "bookmark", to: "posts#destroy_bookmark"
2014-05-13 20:53:11 +08:00
put "wiki"
put "post_type"
put "rebake"
2014-09-23 00:55:13 +08:00
put "unhide"
put "locked"
put "notice"
2013-12-24 07:50:36 +08:00
get "replies"
get "revisions/latest" => "posts#latest_revision"
get "revisions/:revision" => "posts#revisions", :constraints => { revision: /\d+/ }
put "revisions/:revision/hide" => "posts#hide_revision", :constraints => { revision: /\d+/ }
put "revisions/:revision/show" => "posts#show_revision", :constraints => { revision: /\d+/ }
put "revisions/:revision/revert" => "posts#revert", :constraints => { revision: /\d+/ }
FEATURE: Allow admins to permanently delete revisions (#19913) # Context This PR introduces the ability to permanently delete revisions from a post while maintaining the changes implemented by the revisions. Additional Context: /t/90301 # Functionality In the case a staff member wants to _remove the visual cue_ that a post has been edited eg. <img width="86" alt="Screenshot 2023-01-18 at 2 59 12 PM" src="https://user-images.githubusercontent.com/50783505/213293333-9c881229-ab18-4591-b39b-e3419a67907d.png"> while maintaining the changes made in the edits, they can enable the (hidden) site setting of `can_permanently_delete`. When this is enabled, after _hiding_ the revisions <img width="149" alt="Screenshot 2023-01-19 at 1 53 35 PM" src="https://user-images.githubusercontent.com/50783505/213546080-2a9e9c55-b3ef-428e-a93d-1b6ba287dfae.png"> there will be an additional button in the history modal to <kbd>Delete revisions</kbd> on a post. <img width="997" alt="Screenshot 2023-01-19 at 1 49 51 PM" src="https://user-images.githubusercontent.com/50783505/213546333-49042558-50ab-4724-9da7-08bacc68d38d.png"> Since this action is permanent, we display a confirmation dialog prior to triggering the destroy call <img width="722" alt="Screenshot 2023-01-19 at 1 55 59 PM" src="https://user-images.githubusercontent.com/50783505/213546487-96ea6e89-ac49-4892-b4b0-28996e3c867f.png"> Once confirmed the history modal will close and the post will `rebake` to display an _unedited_ post. <img width="868" alt="Screenshot 2023-01-19 at 1 56 35 PM" src="https://user-images.githubusercontent.com/50783505/213546608-d6436717-8484-4132-a1a8-b7a348d92728.png"> see that there is not a visual que for _revision have been made on this post_ for a post that **HAS** been edited. In addition to this, a user history log for `purge_post_revisions` will be added for each action completed. # Limits - Admins are rate limited to 20 posts per minute
2023-01-20 05:09:01 +08:00
delete "revisions/permanently_delete" => "posts#permanently_delete_revisions"
2013-12-24 07:50:36 +08:00
put "recover"
2013-02-06 03:16:51 +08:00
collection do
2013-12-24 07:50:36 +08:00
delete "destroy_many"
put "merge_posts"
2020-06-05 10:49:31 +08:00
end
2013-02-06 03:16:51 +08:00
end
resources :bookmarks, only: %i[create destroy update] do
put "toggle_pin"
end
Improving bookmarks part 1 (#8466) Note: All of this functionality is hidden behind a hidden, default false, site setting called `enable_bookmarks_with_reminders`. Also, any feedback on Ember code would be greatly appreciated! This is part 1 of the bookmark improvements. The next PR will address the backend logic to send reminder notifications for bookmarked posts to users. This PR adds the following functionality: * We are adding a new `bookmarks` table and `Bookmark` model to make the bookmarks a first-class citizen and to allow attaching reminders to them. * Posts now have a new button in their actions menu that has the icon of an actual book * Clicking the button opens the new bookmark modal. * Both name and the reminder type are optional. * If you close the modal without doing anything, the bookmark is saved with no reminder. * If you click the Cancel button, no bookmark is saved at all. * All of the reminder type tiles are dynamic and the times they show will be based on your user timezone set in your profile (this should already be set for you). * If for some reason a user does not have their timezone set they will not be able to set a reminder, but they will still be able to create a bookmark. * A bookmark can be deleted by clicking on the book icon again which will be red if the post is bookmarked. This PR does NOT do anything to migrate or change existing bookmarks in the form of `PostActions`, the two features live side-by-side here. Also this does nothing to the topic bookmarking.
2019-12-11 12:04:02 +08:00
resources :notifications, only: %i[index create update destroy] do
collection do
put "mark-read" => "notifications#mark_read"
# creating an alias cause the api was extended to mark a single notification
# this allows us to cleanly target it
put "read" => "notifications#mark_read"
get "totals" => "notifications#totals"
2020-06-05 10:49:31 +08:00
end
end
match "/auth/failure", to: "users/omniauth_callbacks#failure", via: %i[get post]
get "/auth/:provider", to: "users/omniauth_callbacks#confirm_request"
match "/auth/:provider/callback", to: "users/omniauth_callbacks#complete", via: %i[get post]
get "/associate/:token",
to: "users/associate_accounts#connect_info",
constraints: {
token: /\h{32}/,
}
post "/associate/:token",
to: "users/associate_accounts#connect",
constraints: {
token: /\h{32}/,
}
2013-02-06 03:16:51 +08:00
post "/clicks/track" => "clicks#track", :as => "track_clicks"
2013-02-06 03:16:51 +08:00
resources :post_action_users, only: %i[index]
resources :post_readers, only: %i[index]
resources :post_actions, only: %i[create destroy]
resources :user_actions, only: %i[index show]
2013-02-06 03:16:51 +08:00
resources :badges, only: [:index]
get "/badges/:id(/:slug)" => "badges#show", :constraints => { format: /(json|html|rss)/ }
resources :user_badges, only: %i[index create destroy] do
put "toggle_favorite" => "user_badges#toggle_favorite", :constraints => { format: :json }
end
2014-03-05 20:52:20 +08:00
get "/c", to: redirect(relative_url_root + "categories")
resources :categories, only: %i[index create update destroy]
2015-08-28 01:14:59 +08:00
post "categories/reorder" => "categories#reorder"
get "categories/find" => "categories#find"
post "categories/search" => "categories#search"
get "categories/:parent_category_id" => "categories#index"
scope path: "category/:category_id" do
post "/move" => "categories#move"
post "/notifications" => "categories#set_notifications"
put "/slug" => "categories#update_slug"
end
get "category/*path" => "categories#redirect"
get "categories_and_latest" => "categories#categories_and_latest"
get "categories_and_top" => "categories#categories_and_top"
get "c/:id/show" => "categories#show"
get "c/:id/visible_groups" => "categories#visible_groups"
get "c/*category_slug/find_by_slug" => "categories#find_by_slug"
get "c/*category_slug/edit(/:tab)" => "categories#find_by_slug",
:constraints => {
format: "html",
}
get "/new-category" => "categories#show", :constraints => { format: "html" }
get "c/*category_slug_path_with_id.rss" => "list#category_feed", :format => :rss
scope path: "c/*category_slug_path_with_id" do
get "/none" => "list#category_none_latest"
TopTopic.periods.each do |period|
get "/none/l/top/#{period}", to: redirect("/none/l/top?period=#{period}", status: 301)
get "/l/top/#{period}", to: redirect("/l/top?period=#{period}", status: 301)
end
Discourse.filters.each do |filter|
get "/none/l/#{filter}" => "list#category_none_#{filter}", :as => "category_none_#{filter}"
get "/l/#{filter}" => "list#category_#{filter}", :as => "category_#{filter}"
end
get "/all" => "list#category_default",
:as => "category_all",
:constraints => {
format: "html",
}
get "/subcategories" => "categories#index"
get "/" => "list#category_default", :as => "category_default"
end
FEATURE: Generic hashtag autocomplete lookup and markdown cooking (#18937) This commit fleshes out and adds functionality for the new `#hashtag` search and lookup system, still hidden behind the `enable_experimental_hashtag_autocomplete` feature flag. **Serverside** We have two plugin API registration methods that are used to define data sources (`register_hashtag_data_source`) and hashtag result type priorities depending on the context (`register_hashtag_type_in_context`). Reading the comments in plugin.rb should make it clear what these are doing. Reading the `HashtagAutocompleteService` in full will likely help a lot as well. Each data source is responsible for providing its own **lookup** and **search** method that returns hashtag results based on the arguments provided. For example, the category hashtag data source has to take into account parent categories and how they relate, and each data source has to define their own icon to use for the hashtag, and so on. The `Site` serializer has two new attributes that source data from `HashtagAutocompleteService`. There is `hashtag_icons` that is just a simple array of all the different icons that can be used for allowlisting in our markdown pipeline, and there is `hashtag_context_configurations` that is used to store the type priority orders for each registered context. When sending emails, we cannot render the SVG icons for hashtags, so we need to change the HTML hashtags to the normal `#hashtag` text. **Markdown** The `hashtag-autocomplete.js` file is where I have added the new `hashtag-autocomplete` markdown rule, and like all of our rules this is used to cook the raw text on both the clientside and on the serverside using MiniRacer. Only on the server side do we actually reach out to the database with the `hashtagLookup` function, on the clientside we just render a plainer version of the hashtag HTML. Only in the composer preview do we do further lookups based on this. This rule is the first one (that I can find) that uses the `currentUser` based on a passed in `user_id` for guardian checks in markdown rendering code. This is the `last_editor_id` for both the post and chat message. In some cases we need to cook without a user present, so the `Discourse.system_user` is used in this case. **Chat Channels** This also contains the changes required for chat so that chat channels can be used as a data source for hashtag searches and lookups. This data source will only be used when `enable_experimental_hashtag_autocomplete` is `true`, so we don't have to worry about channel results suddenly turning up. ------ **Known Rough Edges** - Onebox excerpts will not render the icon svg/use tags, I plan to address that in a follow up PR - Selecting a hashtag + pressing the Quote button will result in weird behaviour, I plan to address that in a follow up PR - Mixed hashtag contexts for hashtags without a type suffix will not work correctly, e.g. #ux which is both a category and a channel slug will resolve to a category when used inside a post or within a [chat] transcript in that post. Users can get around this manually by adding the correct suffix, for example ::channel. We may get to this at some point in future - Icons will not show for the hashtags in emails since SVG support is so terrible in email (this is not likely to be resolved, but still noting for posterity) - Additional refinements and review fixes wil
2022-11-21 06:37:06 +08:00
get "hashtags" => "hashtags#lookup"
get "hashtags/by-ids" => "hashtags#by_ids"
get "hashtags/search" => "hashtags#search"
2014-01-14 08:02:14 +08:00
TopTopic.periods.each do |period|
get "top/#{period}.rss", to: redirect("top.rss?period=#{period}", status: 301)
get "top/#{period}.json", to: redirect("top.json?period=#{period}", status: 301)
get "top/#{period}", to: redirect("top?period=#{period}", status: 301)
2013-07-06 04:49:06 +08:00
end
get "latest.rss" => "list#latest_feed", :format => :rss
get "top.rss" => "list#top_feed", :format => :rss
get "hot.rss" => "list#hot_feed", :format => :rss
2014-10-16 11:52:21 +08:00
Discourse.filters.each { |filter| get "#{filter}" => "list##{filter}" }
2013-02-06 03:16:51 +08:00
get "filter" => "list#filter"
2015-04-04 04:55:32 +08:00
get "search/query" => "search#query"
get "search" => "search#show"
post "search/click" => "search#click"
2020-06-05 10:49:31 +08:00
2013-02-06 03:16:51 +08:00
# Topics resource
2013-12-24 07:50:36 +08:00
get "t/:id" => "topics#show"
put "t/:topic_id" => "topics#update", :constraints => { topic_id: /\d+/ }
2014-01-14 08:02:14 +08:00
delete "t/:id" => "topics#destroy"
put "t/:id/archive-message" => "topics#archive_message"
put "t/:id/move-to-inbox" => "topics#move_to_inbox"
put "t/:id/convert-topic/:type" => "topics#convert_topic"
put "t/:id/publish" => "topics#publish"
put "t/:id/shared-draft" => "topics#update_shared_draft"
put "t/:id/reset-bump-date/(:post_id)" => "topics#reset_bump_date",
:constraints => {
id: /\d+/,
post_id: /\d+/,
}
put "topics/bulk"
2014-03-04 04:46:38 +08:00
put "topics/reset-new" => "topics#reset_new"
put "topics/pm-reset-new" => "topics#private_message_reset_new"
2013-12-24 07:50:36 +08:00
post "topics/timings"
2020-06-05 10:49:31 +08:00
get "topics/similar_to" => "similar_topics#index"
resources :similar_topics, only: [:index]
2020-06-05 10:49:31 +08:00
get "topics/feature_stats"
2020-06-05 10:49:31 +08:00
2018-02-14 04:46:25 +08:00
scope "/topics", username: RouteFormat.username do
get "created-by/:username" => "list#topics_by",
:as => "topics_by",
:defaults => {
format: :json,
}
get "private-messages/:username" => "list#private_messages",
:as => "topics_private_messages",
:defaults => {
format: :json,
}
get "private-messages-sent/:username" => "list#private_messages_sent",
:as => "topics_private_messages_sent",
:defaults => {
format: :json,
}
get "private-messages-archive/:username" => "list#private_messages_archive",
:as => "topics_private_messages_archive",
:defaults => {
format: :json,
}
get "private-messages-unread/:username" => "list#private_messages_unread",
:as => "topics_private_messages_unread",
:defaults => {
format: :json,
}
get "private-messages-tags/:username/:tag_id.json" => "list#private_messages_tag",
:as => "topics_private_messages_tag",
:defaults => {
format: :json,
}
get "private-messages-new/:username" => "list#private_messages_new",
:as => "topics_private_messages_new",
:defaults => {
format: :json,
}
get "private-messages-warnings/:username" => "list#private_messages_warnings",
:as => "topics_private_messages_warnings",
:defaults => {
format: :json,
}
get "groups/:group_name" => "list#group_topics",
:as => "group_topics",
:group_name => RouteFormat.username
2020-06-05 10:49:31 +08:00
2018-02-14 04:46:25 +08:00
scope "/private-messages-group/:username", group_name: RouteFormat.username do
get ":group_name.json" => "list#private_messages_group",
:as => "topics_private_messages_group"
get ":group_name/archive.json" => "list#private_messages_group_archive",
:as => "topics_private_messages_group_archive"
get ":group_name/new.json" => "list#private_messages_group_new",
:as => "topics_private_messages_group_new"
get ":group_name/unread.json" => "list#private_messages_group_unread",
:as => "topics_private_messages_group_unread"
2020-06-05 10:49:31 +08:00
end
2018-02-14 04:46:25 +08:00
end
get "embed/topics" => "embed#topics"
2014-01-04 01:52:24 +08:00
get "embed/comments" => "embed#comments"
get "embed/count" => "embed#count"
get "embed/info" => "embed#info"
2020-06-05 10:49:31 +08:00
get "new-topic" => "new_topic#index"
get "new-message" => "new_topic#index"
2020-06-05 10:49:31 +08:00
# Topic routes
get "t/id_for/:slug" => "topics#id_for_slug"
get "t/external_id/:external_id" => "topics#show_by_external_id",
:format => :json,
:constraints => {
external_id: /[\w-]+/,
}
get "t/:slug/:topic_id/print" => "topics#show",
:format => :html,
:print => "true",
:constraints => {
topic_id: /\d+/,
}
2013-12-24 07:50:36 +08:00
get "t/:slug/:topic_id/wordpress" => "topics#wordpress", :constraints => { topic_id: /\d+/ }
get "t/:topic_id/wordpress" => "topics#wordpress", :constraints => { topic_id: /\d+/ }
get "t/:slug/:topic_id/summary" => "topics#show",
:defaults => {
summary: true,
},
:constraints => {
topic_id: /\d+/,
}
get "t/:topic_id/summary" => "topics#show", :constraints => { topic_id: /\d+/ }
get "t/:topic_id/strategy-summary" => "topics#summary",
:constraints => {
topic_id: /\d+/,
},
:format => :json
2013-12-24 07:50:36 +08:00
put "t/:slug/:topic_id" => "topics#update", :constraints => { topic_id: /\d+/ }
put "t/:slug/:topic_id/status" => "topics#status", :constraints => { topic_id: /\d+/ }
put "t/:topic_id/status" => "topics#status", :constraints => { topic_id: /\d+/ }
put "t/:topic_id/clear-pin" => "topics#clear_pin", :constraints => { topic_id: /\d+/ }
put "t/:topic_id/re-pin" => "topics#re_pin", :constraints => { topic_id: /\d+/ }
2013-12-24 07:50:36 +08:00
put "t/:topic_id/mute" => "topics#mute", :constraints => { topic_id: /\d+/ }
put "t/:topic_id/unmute" => "topics#unmute", :constraints => { topic_id: /\d+/ }
post "t/:topic_id/timer" => "topics#timer", :constraints => { topic_id: /\d+/ }
2014-06-17 00:28:07 +08:00
put "t/:topic_id/make-banner" => "topics#make_banner", :constraints => { topic_id: /\d+/ }
put "t/:topic_id/remove-banner" => "topics#remove_banner", :constraints => { topic_id: /\d+/ }
2013-12-24 07:50:36 +08:00
put "t/:topic_id/remove-allowed-user" => "topics#remove_allowed_user",
:constraints => {
topic_id: /\d+/,
}
put "t/:topic_id/remove-allowed-group" => "topics#remove_allowed_group",
:constraints => {
topic_id: /\d+/,
}
2013-12-24 07:50:36 +08:00
put "t/:topic_id/recover" => "topics#recover", :constraints => { topic_id: /\d+/ }
get "t/:topic_id/:post_number" => "topics#show",
:constraints => {
topic_id: /\d+/,
post_number: /\d+/,
}
get "t/:topic_id/last" => "topics#show",
:post_number => 99_999_999,
2013-12-24 07:50:36 +08:00
:constraints => {
topic_id: /\d+/,
}
get "t/:slug/:topic_id.rss" => "topics#feed",
:format => :rss,
:constraints => {
2013-12-24 07:50:36 +08:00
topic_id: /\d+/,
}
2013-12-24 07:50:36 +08:00
get "t/:slug/:topic_id" => "topics#show", :constraints => { topic_id: /\d+/ }
get "t/:slug/:topic_id/:post_number" => "topics#show",
:constraints => {
topic_id: /\d+/,
post_number: /\d+/,
}
get "t/:slug/:topic_id/last" => "topics#show",
:post_number => 99_999_999,
:constraints => {
topic_id: /\d+/,
}
get "t/:topic_id/posts" => "topics#posts", :constraints => { topic_id: /\d+/ }, :format => :json
get "t/:topic_id/post_ids" => "topics#post_ids",
:constraints => {
topic_id: /\d+/,
},
:format => :json
get "t/:topic_id/excerpts" => "topics#excerpts",
:constraints => {
2013-12-24 07:50:36 +08:00
topic_id: /\d+/,
},
:format => :json
2013-12-24 07:50:36 +08:00
post "t/:topic_id/timings" => "topics#timings", :constraints => { topic_id: /\d+/ }
post "t/:topic_id/invite" => "topics#invite", :constraints => { topic_id: /\d+/ }
post "t/:topic_id/invite-group" => "topics#invite_group", :constraints => { topic_id: /\d+/ }
2013-12-24 07:50:36 +08:00
post "t/:topic_id/move-posts" => "topics#move_posts", :constraints => { topic_id: /\d+/ }
post "t/:topic_id/merge-topic" => "topics#merge_topic", :constraints => { topic_id: /\d+/ }
post "t/:topic_id/change-owner" => "topics#change_post_owners",
:constraints => {
topic_id: /\d+/,
}
put "t/:topic_id/change-timestamp" => "topics#change_timestamps",
:constraints => {
2013-12-24 07:50:36 +08:00
topic_id: /\d+/,
}
2013-12-24 07:50:36 +08:00
delete "t/:topic_id/timings" => "topics#destroy_timings", :constraints => { topic_id: /\d+/ }
2015-01-12 19:10:15 +08:00
put "t/:topic_id/bookmark" => "topics#bookmark", :constraints => { topic_id: /\d+/ }
put "t/:topic_id/remove_bookmarks" => "topics#remove_bookmarks",
:constraints => {
topic_id: /\d+/,
}
put "t/:topic_id/tags" => "topics#update_tags", :constraints => { topic_id: /\d+/ }
put "t/:topic_id/slow_mode" => "topics#set_slow_mode", :constraints => { topic_id: /\d+/ }
2013-12-24 07:50:36 +08:00
post "t/:topic_id/notifications" => "topics#set_notifications",
:constraints => {
topic_id: /\d+/,
}
2020-06-05 10:49:31 +08:00
get "p/:post_id(/:user_id)" => "posts#short_link"
get "/posts/:id/cooked" => "posts#cooked"
2014-04-02 05:45:16 +08:00
get "/posts/:id/expand-embed" => "posts#expand_embed"
get "/posts/:id/raw" => "posts#markdown_id"
2014-10-18 03:18:29 +08:00
get "/posts/:id/raw-email" => "posts#raw_email"
get "raw/:topic_id(/:post_number)" => "posts#markdown_num"
2020-06-05 10:49:31 +08:00
resources :invites, only: %i[create update destroy]
get "/invites/:id" => "invites#show", :constraints => { format: :html }
post "invites/create-multiple" => "invites#create_multiple", :constraints => { format: :json }
2020-06-05 10:49:31 +08:00
2016-12-05 00:06:35 +08:00
post "invites/upload_csv" => "invites#upload_csv"
post "invites/destroy-all-expired" => "invites#destroy_all_expired"
2014-10-07 02:48:56 +08:00
post "invites/reinvite" => "invites#resend_invite"
post "invites/reinvite-all" => "invites#resend_all_invites"
2013-12-24 07:50:36 +08:00
delete "invites" => "invites#destroy"
put "invites/show/:id" => "invites#perform_accept_invitation", :as => "perform_accept_invite"
get "invites/retrieve" => "invites#retrieve"
2020-06-05 10:49:31 +08:00
post "/export_csv/export_entity" => "export_csv#export_entity",
:as => "export_entity_export_csv_index"
2014-12-23 00:17:04 +08:00
2013-12-24 07:50:36 +08:00
get "onebox" => "onebox#show"
get "inline-onebox" => "inline_onebox#show"
2013-02-06 03:16:51 +08:00
2014-06-17 02:25:33 +08:00
get "exception" => "list#latest"
2013-02-06 03:16:51 +08:00
2013-12-24 07:50:36 +08:00
get "message-bus/poll" => "message_bus#poll"
2013-02-06 03:16:51 +08:00
resources :drafts, only: %i[index create show destroy]
2013-02-06 03:16:51 +08:00
get "/service-worker.js" => "static#service_worker_asset", :format => :js
if service_worker_asset = Rails.application.assets_manifest.assets["service-worker.js"]
# https://developers.google.com/web/fundamentals/codelabs/debugging-service-workers/
# Normally the browser will wait until a user closes all tabs that contain the
# current site before updating to a new Service Worker.
# Support the old Service Worker path to avoid routing error filling up the
# logs.
get service_worker_asset => "static#service_worker_asset", :format => :js
end
get "cdn_asset/:site/*path" => "static#cdn_asset",
:format => false,
:constraints => {
format: /.*/,
}
2020-06-05 10:49:31 +08:00
get "favicon/proxied" => "static#favicon", :format => false
2020-06-05 10:49:31 +08:00
2013-12-24 07:50:36 +08:00
get "robots.txt" => "robots_txt#index"
get "robots-builder.json" => "robots_txt#builder"
get "offline.html" => "offline#index"
get "manifest.webmanifest" => "metadata#manifest", :as => :manifest
get "manifest.json" => "metadata#manifest"
get ".well-known/assetlinks.json" => "metadata#app_association_android"
get "apple-app-site-association" => "metadata#app_association_ios", :format => false
get "opensearch" => "metadata#opensearch", :constraints => { format: :xml }
2020-06-05 10:49:31 +08:00
scope "/tag/:tag_id" do
constraints format: :json do
get "/" => "tags#show", :as => "tag_show"
get "/info" => "tags#info"
get "/notifications" => "tags#notifications"
put "/notifications" => "tags#update_notifications"
put "/" => "tags#update"
delete "/" => "tags#destroy"
post "/synonyms" => "tags#create_synonyms"
delete "/synonyms/:synonym_id" => "tags#destroy_synonym"
Discourse.filters.each do |filter|
get "/l/#{filter}" => "tags#show_#{filter}", :as => "tag_show_#{filter}"
2020-06-05 10:49:31 +08:00
end
end
constraints format: :rss do
get "/" => "tags#tag_feed"
2020-06-05 10:49:31 +08:00
end
end
scope "/tags" do
get "/" => "tags#index"
get "/filter/list" => "tags#index"
get "/filter/search" => "tags#search"
get "/list" => "tags#list"
get "/personal_messages/:username" => "tags#personal_messages",
:constraints => {
username: RouteFormat.username,
}
2018-10-15 16:12:54 +08:00
post "/upload" => "tags#upload"
get "/unused" => "tags#list_unused"
delete "/unused" => "tags#destroy_unused"
constraints(tag_id: %r{[^/]+?}, format: /json|rss/) do
scope path: "/c/*category_slug_path_with_id" do
Discourse.filters.each do |filter|
get "/none/:tag_id/l/#{filter}" => "tags#show_#{filter}",
:as => "tag_category_none_show_#{filter}",
:defaults => {
no_subcategories: true,
}
get "/all/:tag_id/l/#{filter}" => "tags#show_#{filter}",
:as => "tag_category_all_show_#{filter}",
:defaults => {
no_subcategories: false,
}
end
get "/none/:tag_id" => "tags#show",
:as => "tag_category_none_show",
:defaults => {
no_subcategories: true,
}
get "/all/:tag_id" => "tags#show",
:as => "tag_category_all_show",
:defaults => {
no_subcategories: false,
}
Discourse.filters.each do |filter|
get "/:tag_id/l/#{filter}" => "tags#show_#{filter}",
:as => "tag_category_show_#{filter}"
2020-06-05 10:49:31 +08:00
end
get "/:tag_id" => "tags#show", :as => "tag_category_show"
end
get "/intersection/:tag_id/*additional_tag_ids" => "tags#show", :as => "tag_intersection"
end
get "*tag_id", to: redirect(relative_url_root + "tag/%{tag_id}")
end
resources :tag_groups, constraints: StaffConstraint.new, except: [:edit]
get "/tag_groups/filter/search" => "tag_groups#search", :format => :json
2013-12-24 07:50:36 +08:00
Discourse.filters.each do |filter|
root to: "list##{filter}",
constraints: HomePageConstraint.new("#{filter}"),
as: "list_#{filter}"
end
get "/t/:topic_id/view-stats.json" => "topic_view_stats#index"
# special case for categories
root to: "categories#index",
constraints: HomePageConstraint.new("categories"),
as: "categories_index"
2020-06-05 10:49:31 +08:00
root to: "finish_installation#index",
constraints: HomePageConstraint.new("finish_installation"),
as: "installation_redirect"
2020-06-05 10:49:31 +08:00
root to: "custom#index", constraints: HomePageConstraint.new("custom"), as: "custom_index"
get "/user-api-key/new" => "user_api_keys#new"
post "/user-api-key" => "user_api_keys#create"
post "/user-api-key/revoke" => "user_api_keys#revoke"
post "/user-api-key/undo-revoke" => "user_api_keys#undo_revoke"
get "/user-api-key/otp" => "user_api_keys#otp"
post "/user-api-key/otp" => "user_api_keys#create_otp"
2020-06-05 10:49:31 +08:00
get "/safe-mode" => "safe_mode#index"
post "/safe-mode" => "safe_mode#enter", :as => "safe_mode_enter"
2020-06-05 10:49:31 +08:00
get "/theme-qunit" => "qunit#theme"
DEV: Run QUnit tests for official Discourse themes (#24405) Why this change? As the number of themes which the Discourse team supports officially grows, we want to ensure that changes made to Discourse core do not break the plugins. As such, we are adding a step to our Github actions test job to run the QUnit tests for all official themes. What does this change do? This change adds a new job to our tests Github actions workflow to run the QUnit tests for all official plugins. This is achieved with the following changes: 1. Update `testem.js` to rely on the `THEME_TEST_PAGES` env variable to set the `test_page` option when running theme QUnit tests with testem. The `test_page` option [allows an array to be specified](https://github.com/testem/testem#multiple-test-pages) such that tests for multiple pages can be run at the same time. We are relying on a ENV variable because the `testem` CLI does not support passing a list of pages to the `--test_page` option. 2. Support a `/testem-theme-qunit/:testem_id/theme-qunit` Rails route in the development environment. This is done because testem prefixes the path with a unique ID to the configured `test_page` URL. This is problematic for us because we proxy all testem requests to the Rails server and testem's proxy configuration option does not allow us to easily rewrite the URL to remove the prefix. Therefore, we configure a proxy in testem to prefix `theme-qunit` requests with `/testem-theme-qunit` which can then be easily identified by the Rails server and routed accordingly. 3. Update `qunit:test` to support a `THEME_IDS` environment variable which will allow it to run QUnit tests for multiple themes at the same time. 4. Support `bin/rake themes:qunit[ids,"<theme_id>|<theme_id>"]` to run the QUnit tests for multiple themes at the same time. 5. Adds a `themes:qunit_all_official` Rake task which runs the QUnit tests for all the official themes.
2023-11-17 07:17:32 +08:00
# This is a special route that is used when theme QUnit tests are run through testem which appends a testem_id to the
# path. Unfortunately, testem's proxy support does not allow us to easily remove this from the path, so we have to
# handle it here.
if Rails.env.development?
get "/testem-theme-qunit/:testem_id/theme-qunit" => "qunit#theme",
:constraints => {
testem_id: /\d+/,
}
end
post "/push_notifications/subscribe" => "push_notification#subscribe"
post "/push_notifications/unsubscribe" => "push_notification#unsubscribe"
resources :csp_reports, only: [:create]
get "/permalink-check", to: "permalinks#check"
2020-12-18 23:03:51 +08:00
post "/do-not-disturb" => "do_not_disturb#create"
delete "/do-not-disturb" => "do_not_disturb#destroy"
DEV: Introduce PresenceChannel API for core and plugin use PresenceChannel aims to be a generic system for allow the server, and end-users, to track the number and identity of users performing a specific task on the site. For example, it might be used to track who is currently 'replying' to a specific topic, editing a specific wiki post, etc. A few key pieces of information about the system: - PresenceChannels are identified by a name of the format `/prefix/blah`, where `prefix` has been configured by some core/plugin implementation, and `blah` can be any string the implementation wants to use. - Presence is a boolean thing - each user is either present, or not present. If a user has multiple clients 'present' in a channel, they will be deduplicated so that the user is only counted once - Developers can configure the existence and configuration of channels 'just in time' using a callback. The result of this is cached for 2 minutes. - Configuration of a channel can specify permissions in a similar way to MessageBus (public boolean, a list of allowed_user_ids, and a list of allowed_group_ids). A channel can also be placed in 'count_only' mode, where the identity of present users is not revealed to end-users. - The backend implementation uses redis lua scripts, and is designed to scale well. In the future, hard limits may be introduced on the maximum number of users that can be present in a channel. - Clients can enter/leave at will. If a client has not marked itself 'present' in the last 60 seconds, they will automatically 'leave' the channel. The JS implementation takes care of this regular check-in. - On the client-side, PresenceChannel instances can be fetched from the `presence` ember service. Each PresenceChannel can be used entered/left/subscribed/unsubscribed, and the service will automatically deduplicate information before interacting with the server. - When a client joins a PresenceChannel, the JS implementation will automatically make a GET request for the current channel state. To avoid this, the channel state can be serialized into one of your existing endpoints, and then passed to the `subscribe` method on the channel. - The PresenceChannel JS object is an ember object. The `users` and `count` property can be used directly in ember templates, and in computed properties. - It is important to make sure that you `unsubscribe()` and `leave()` any PresenceChannel objects after use An example implementation may look something like this. On the server: ```ruby register_presence_channel_prefix("site") do |channel| next nil unless channel == "/site/online" PresenceChannel::Config.new(public: true) end ``` And on the client, a component could be implemented like this: ```javascript import Component from "@ember/component"; import { inject as service } from "@ember/service"; export default Component.extend({ presence: service(), init() { this._super(...arguments); this.set("presenceChannel", this.presence.getChannel("/site/online")); }, didInsertElement() { this.presenceChannel.enter(); this.presenceChannel.subscribe(); }, willDestroyElement() { this.presenceChannel.leave(); this.presenceChannel.unsubscribe(); }, }); ``` With this template: ```handlebars Online: {{presenceChannel.count}} <ul> {{#each presenceChannel.users as |user|}} <li>{{avatar user imageSize="tiny"}} {{user.username}}</li> {{/each}} </ul> ```
2021-08-27 21:43:39 +08:00
post "/presence/update" => "presence#update"
get "/presence/get" => "presence#get"
get "user-status" => "user_status#get"
2022-05-27 17:15:14 +08:00
put "user-status" => "user_status#set"
delete "user-status" => "user_status#clear"
resources :sidebar_sections, only: %i[index create update destroy]
put "/sidebar_sections/reset/:id" => "sidebar_sections#reset"
post "/pageview" => "pageview#index"
get "*url", to: "permalinks#show", constraints: PermalinkConstraint.new
get "/form-templates/:id" => "form_templates#show"
get "/form-templates" => "form_templates#index"
end
2013-02-06 03:16:51 +08:00
end