diff --git a/app/controllers/api/v0/health_checks_controller.rb b/app/controllers/api/v0/health_checks_controller.rb new file mode 100644 index 000000000..5b1cb4826 --- /dev/null +++ b/app/controllers/api/v0/health_checks_controller.rb @@ -0,0 +1,37 @@ +module Api + module V0 + class HealthChecksController < ApiController + before_action :authenticate_with_token + + def app + render json: { message: "App is up!" }, status: :ok + end + + def search + if Search::Client.ping + render json: { message: "Search ping succeeded!" }, status: :ok + else + render json: { message: "Search ping failed!" }, status: :internal_server_error + end + end + + def database + if ActiveRecord::Base.connected? + render json: { message: "Database connected" }, status: :ok + else + render json: { message: "Database NOT connected!" }, status: :internal_server_error + end + end + + private + + def authenticate_with_token + key = request.headers["health-check-token"] + + return if key == SiteConfig.health_check_token + + error_unauthorized + end + end + end +end diff --git a/app/models/site_config.rb b/app/models/site_config.rb index 95878c7c3..879da4719 100644 --- a/app/models/site_config.rb +++ b/app/models/site_config.rb @@ -116,4 +116,7 @@ class SiteConfig < RailsSettings::Base # Shop field :shop_url, type: :string, default: "https://shop.dev.to" + + # Special API tokens + field :health_check_token, type: :string, default: "secret" end diff --git a/app/views/internal/configs/show.html.erb b/app/views/internal/configs/show.html.erb index 227f5cff0..cd98a7761 100644 --- a/app/views/internal/configs/show.html.erb +++ b/app/views/internal/configs/show.html.erb @@ -293,7 +293,6 @@
URL campaign sidebar image will link to
-
<%= f.label :campaign_featured_tags %> <%= f.text_field :campaign_featured_tags, @@ -596,6 +595,27 @@
+ +
+ <%= render partial: "card_header", + locals: { + header: "API", + state: "collapse", + target: "apiBodyContainer", + expanded: "false" + } %> +
+
+ <%= f.label :health_check_token, "Health Check Token" %> + <%= f.text_field :health_check_token, + class: "form-control", + value: SiteConfig.health_check_token, + placeholder: "a secure token" %> +
Used to authenticate with your health check endpoints.
+
+
+
+ <% if current_user.has_role?(:single_resource_admin, Config) %>
Confirm and Submit
diff --git a/config/routes.rb b/config/routes.rb index a06adafca..a0b932232 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -143,6 +143,14 @@ Rails.application.routes.draw do get "/analytics/historical", to: "analytics#historical" get "/analytics/past_day", to: "analytics#past_day" get "/analytics/referrers", to: "analytics#referrers" + + resources :health_checks, only: [] do + collection do + get :app + get :search + get :database + end + end end end diff --git a/spec/requests/api/v0/health_checks_spec.rb b/spec/requests/api/v0/health_checks_spec.rb new file mode 100644 index 000000000..4e2fcb977 --- /dev/null +++ b/spec/requests/api/v0/health_checks_spec.rb @@ -0,0 +1,53 @@ +require "rails_helper" + +RSpec.describe "HealthCheck", type: :request do + let(:token) { "secret" } + let(:headers) { { "health-check-token" => token } } + + before { SiteConfig.health_check_token = token } + + context "without a token" do + it "returns an unauthorized request" do + get app_api_health_checks_path + expect(response.status).to eq(401) + end + end + + describe "GET /api/health_checks/app" do + it "returns json success" do + get app_api_health_checks_path, headers: headers + expect(response.status).to eq(200) + expect(response.parsed_body["message"]).to eq("App is up!") + end + end + + describe "GET /api/health_checks/search" do + it "returns json success if ping succeeds" do + get search_api_health_checks_path, headers: headers + expect(response.status).to eq(200) + expect(response.parsed_body["message"]).to eq("Search ping succeeded!") + end + + it "returns json failure if ping fails" do + allow(Search::Client).to receive(:ping).and_return(false) + get search_api_health_checks_path, headers: headers + expect(response.status).to eq(500) + expect(response.parsed_body["message"]).to eq("Search ping failed!") + end + end + + describe "GET /api/health_checks/database" do + it "returns json success if connection check succeeds" do + get database_api_health_checks_path, headers: headers + expect(response.status).to eq(200) + expect(response.parsed_body["message"]).to eq("Database connected") + end + + it "returns json failure if connection check fails" do + allow(ActiveRecord::Base).to receive(:connected?).and_return(false) + get database_api_health_checks_path, headers: headers + expect(response.status).to eq(500) + expect(response.parsed_body["message"]).to eq("Database NOT connected!") + end + end +end