21
1
Fork 0
mirror of https://github.com/Evolix/chexpire.git synced 2024-04-27 06:20:50 +02:00

Merge pull request #90 from Evolix/unsupported-whois

Manual mode for unsupported TLDs
This commit is contained in:
Colin Darie 2018-08-30 19:04:27 +02:00 committed by GitHub
commit 0f3571b3bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 248 additions and 38 deletions

View file

@ -3,7 +3,7 @@
class ChecksController < ApplicationController class ChecksController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_check, except: [:index, :new, :create] before_action :set_check, except: [:index, :new, :create, :supports]
after_action :verify_authorized, except: :index after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index after_action :verify_policy_scoped, only: :index
@ -65,6 +65,11 @@ class ChecksController < ApplicationController
redirect_to checks_path redirect_to checks_path
end end
def supports
@check = Check.new(new_check_params)
authorize @check
end
private private
def set_check def set_check
@ -82,7 +87,7 @@ class ChecksController < ApplicationController
def check_params(*others) def check_params(*others)
params.require(:check) params.require(:check)
.permit(:domain, :domain_created_at, :comment, :vendor, :round_robin, *others, .permit(:domain, :domain_expires_at, :comment, :vendor, :round_robin, *others,
notifications_attributes: [:id, :channel, :recipient, :interval]) notifications_attributes: [:id, :channel, :recipient, :interval])
end end

View file

@ -0,0 +1,57 @@
// Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
// License: GNU AGPL-3+ (see full text in LICENSE file)
function checkValidationInitialize() {
const element = document.getElementById("check_domain");
if (element && element.dataset.kind == "domain") {
addEventSupportListener(element);
}
}
function addEventSupportListener(element) {
element.addEventListener("blur", event => {
const request = $.ajax("/checks/supports.json", {
method: "post",
dataType: "json",
data: {
check: {
domain: event.target.value,
kind: element.dataset.kind,
}
}
})
request.done(response => {
const { supported } = response.check;
toggleUnsupportedContainers(supported);
setFocus(supported);
// set normalized domain
element.value = response.check.domain;
});
});
}
function toggleUnsupportedContainers(supported) {
const containerClass = supported ? "d-none" : "d-block";
document.getElementById("check_domain_expires_at_container").className = containerClass;
const domainHint = document.getElementById("check_domain_unsupported_container");
domainHint.classList.remove("d-none");
domainHint.classList.remove("d-block");
domainHint.classList.add(containerClass);
}
function setFocus(supported) {
if (supported) {
return;
}
document.getElementById("check_domain_expires_at").focus();
}
export default checkValidationInitialize;

View file

@ -20,9 +20,13 @@ import 'bootstrap/js/dist/tooltip';
import '../scss'; import '../scss';
import checkValidationInitialize from '../components/check_validation';
Rails.start() Rails.start()
Turbolinks.start() Turbolinks.start()
document.addEventListener("turbolinks:load", () => { document.addEventListener("turbolinks:load", () => {
$('[data-toggle="tooltip"]').tooltip(); $('[data-toggle="tooltip"]').tooltip();
checkValidationInitialize();
}); });

View file

@ -1,7 +1,7 @@
// Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr> // Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
// License: GNU AGPL-3+ (see full text in LICENSE file) // License: GNU AGPL-3+ (see full text in LICENSE file)
$input-placeholder-color: #013d3a; $input-placeholder-color: #b9bbbb;
$enable-rounded: false; $enable-rounded: false;
$theme-colors: ( $theme-colors: (
"primary": #118b83, //light-green "primary": #118b83, //light-green

View file

@ -20,9 +20,6 @@ class WhoisSyncJob < ApplicationJob
return unless response.valid? return unless response.valid?
update_from_response(response) update_from_response(response)
rescue Whois::DomainNotFoundError
check.active = false
check.save!
end end
def update_from_response(response) def update_from_response(response)

View file

@ -15,6 +15,7 @@
# kind :integer not null # kind :integer not null
# last_run_at :datetime # last_run_at :datetime
# last_success_at :datetime # last_success_at :datetime
# mode :integer default("auto"), not null
# round_robin :boolean default(TRUE) # round_robin :boolean default(TRUE)
# vendor :string(255) # vendor :string(255)
# created_at :datetime not null # created_at :datetime not null
@ -39,6 +40,7 @@ class Check < ApplicationRecord
reject_if: lambda { |at| at["recipient"].blank? && at["interval"].blank? } reject_if: lambda { |at| at["recipient"].blank? && at["interval"].blank? }
enum kind: [:domain, :ssl] enum kind: [:domain, :ssl]
enum mode: [:auto, :manual]
self.skip_time_zone_conversion_for_attributes = [ self.skip_time_zone_conversion_for_attributes = [
:domain_created_at, :domain_created_at,
@ -50,10 +52,12 @@ class Check < ApplicationRecord
validates :domain, presence: true validates :domain, presence: true
validate :domain_created_at_past validate :domain_created_at_past
validate :domain_updated_at_past validate :domain_updated_at_past
validates :domain_expires_at, presence: true, unless: :supported?
validates :comment, length: { maximum: 255 } validates :comment, length: { maximum: 255 }
validates :vendor, length: { maximum: 255 } validates :vendor, length: { maximum: 255 }
before_save :reset_consecutive_failures before_save :reset_consecutive_failures
before_save :set_mode
after_update :reset_notifications after_update :reset_notifications
after_save :enqueue_sync after_save :enqueue_sync
@ -84,6 +88,20 @@ class Check < ApplicationRecord
save! save!
end end
def supported?
return true unless domain?
return true if domain.blank?
begin
Whois::Parser.for(domain)
true
rescue Whois::UnsupportedDomainError
false
rescue StandardError
false
end
end
private private
def domain_created_at_past def domain_created_at_past
@ -113,4 +131,9 @@ class Check < ApplicationRecord
self.consecutive_failures = 0 self.consecutive_failures = 0
end end
def set_mode
return unless domain_changed?
self.mode = supported? ? :auto : :manual
end
end end

View file

@ -20,6 +20,10 @@ class CheckPolicy < ApplicationPolicy
owner? owner?
end end
def supports?
new?
end
private private
def owner? def owner?

View file

@ -54,6 +54,7 @@ module CheckProcessor
def base_scope def base_scope
Check Check
.active .active
.auto
.where("last_run_at IS NULL OR last_run_at < DATE_SUB(NOW(), INTERVAL 12 HOUR)") .where("last_run_at IS NULL OR last_run_at < DATE_SUB(NOW(), INTERVAL 12 HOUR)")
end end

View file

@ -3,13 +3,33 @@
<%= simple_form_for(check) do |f| %> <%= simple_form_for(check) do |f| %>
<%= f.input :domain, <%= f.input :domain,
autofocus: true, autofocus: true,
input_html: { autocapitalize: :none, autocorrect: :off }, input_html: { autocapitalize: :none, autocorrect: :off, data: { kind: check.kind } },
label: t(".#{check.kind || "generic" }.domain") %> label: t(".#{check.kind || "generic" }.domain"),
hint: t(".#{check.kind || "generic" }.unsupported"),
hint_html: {
id: "check_domain_unsupported_container",
class: "#{check.supported? && 'd-none'}",
}
%>
<% if check.new_record? %> <% if check.new_record? %>
<%= f.input :kind, as: check.kind.present? ? :hidden : :radio_buttons, collection: Check.kinds.keys %> <%= f.input :kind, as: check.kind.present? ? :hidden : :radio_buttons, collection: Check.kinds.keys %>
<% end %> <% end %>
<div id="check_domain_expires_at_container" class="<%= check.supported? ? "d-none" : "d-block" %>">
<%= f.input :domain_expires_at,
required: true,
input_html: {
type: :date,
value: check.domain_expires_at&.to_date,
min: Date.yesterday,
max: 10.years.from_now.end_of_year.to_date
},
as: :string,
placeholder: t(".domain_expires_at_placeholder")
%>
</div>
<%= f.input :comment %> <%= f.input :comment %>
<%= f.input :vendor %> <%= f.input :vendor %>

View file

@ -0,0 +1,7 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file)
json.check do
json.supported @check.supported?
json.domain normalize_domain(@check.domain)
end

View file

@ -72,17 +72,17 @@ en:
sign_in: "Log in" sign_in: "Log in"
sign_out: "Log out" sign_out: "Log out"
profile: "Profile" profile: "Profile"
home_header: home_header:
welcome: "Chexpire" welcome: "Chexpire"
intro: "Never forget to renew a domain name or SSL certificate." intro: "Never forget to renew a domain name or SSL certificate."
beta_banner: beta_banner:
beta_info: "Chexpire is in \"beta\" release: only few TLD (.com/.net/.org/.fr) are verified for domain name checks and TLS 1.2 is not supported for SSL checks." beta_info: "Chexpire is in \"beta\" release: only few TLD (.com/.net/.org/.fr) are verified for domain name checks and TLS 1.2 is not supported for SSL checks."
issue_link: "Please report issues." issue_link: "Please report issues."
pages: pages:
home: home:
why: "Why Chexpire?" why: "Why Chexpire?"
description: "Chexpire is a Free Software (AGPLv3 license) to manage the expiration of domain names and SSL certificates. It is primarily an ergonomic web interface that allows you easily to add new domain names/SSL certificates to monitor, and custom/unlimited notifications to be notified before expiration." description: "Chexpire is a Free Software (AGPLv3 license) to manage the expiration of domain names and SSL certificates. It is primarily an ergonomic web interface that allows you easily to add new domain names/SSL certificates to monitor, and custom/unlimited notifications to be notified before expiration."
centralization: "Centralize all your expiry dates" centralization: "Centralize all your expiry dates"
centralization-details: "Do you have domain names at different registrars? many Let's Encrypt SSL certificates with automatic renewal? You will enjoy everything centralized in a web interface: adding a domain name/SSL certificate in two clicks, sorted list, search bar etc." centralization-details: "Do you have domain names at different registrars? many Let's Encrypt SSL certificates with automatic renewal? You will enjoy everything centralized in a web interface: adding a domain name/SSL certificate in two clicks, sorted list, search bar etc."
@ -119,6 +119,10 @@ en:
domain: Domain domain: Domain
domain: domain:
domain: Domain name domain: Domain name
unsupported: |
This top-level domain isn't currently automatically supported.
You'll have to fill and maintain yourself the expiry date.
domain_expires_at_placeholder: YYYY-MM-DD.
ssl: ssl:
domain: Hostname domain: Hostname
notifications_hint: | notifications_hint: |

View file

@ -11,6 +11,7 @@ fr:
kind: Type kind: Type
domain_created_at: "Date de création" domain_created_at: "Date de création"
domain_updated_at: "Date de modification" domain_updated_at: "Date de modification"
domain_expires_at: "Date d'expiration"
notification: notification:
interval: Délai interval: Délai
recipient: Destinataire recipient: Destinataire
@ -104,17 +105,17 @@ fr:
sign_in: "Connexion" sign_in: "Connexion"
sign_out: "Déconnexion" sign_out: "Déconnexion"
profile: "Profil" profile: "Profil"
home_header: home_header:
welcome: "Chexpire" welcome: "Chexpire"
intro: "vous n'oublierez plus jamais de renouveler un nom de domaine ou un certificat SSL." intro: "vous n'oublierez plus jamais de renouveler un nom de domaine ou un certificat SSL."
beta_banner: beta_banner:
beta_info: "Chexpire est en version \"beta\" : seuls certains TLD (.com/.net/.org/.fr) sont vérifiés pour les noms de domaine et TLS 1.2 n'est pas supporté pour les vérifications SSL." beta_info: "Chexpire est en version \"beta\" : seuls certains TLD (.com/.net/.org/.fr) sont vérifiés pour les noms de domaine et TLS 1.2 n'est pas supporté pour les vérifications SSL."
issue_link: "Merci de nous reporter bugs et suggestions." issue_link: "Merci de nous reporter bugs et suggestions."
pages: pages:
home: home:
why: "Pourquoi Chexpire ?" why: "Pourquoi Chexpire ?"
description: "Chexpire est un Logiciel Libre (licence AGPLv3) permettant de gérer l'expiration de noms de domaine et de certificats SSL. C'est avant tout une interface web ergonomique permettant d'ajouter simplement de nouveaux noms de domaine/certificats SSL à surveiller, et des notifications sur mesure et illimitées pour être averti·e avant expiration." description: "Chexpire est un Logiciel Libre (licence AGPLv3) permettant de gérer l'expiration de noms de domaine et de certificats SSL. C'est avant tout une interface web ergonomique permettant d'ajouter simplement de nouveaux noms de domaine/certificats SSL à surveiller, et des notifications sur mesure et illimitées pour être averti·e avant expiration."
centralization: "Centralisez toutes vos dates d'expiration" centralization: "Centralisez toutes vos dates d'expiration"
centralization-details: "Vous avez des noms de domaine chez différents registrars ? de nombreux certificats SSL Let's Encrypt en renouvellement automatique ? Vous allez apprécier de tout centraliser simplement dans une interface web : ajout d'un nom de domaine/certificat SSL en deux clics, liste récapitulative triée, barre de recherche etc." centralization-details: "Vous avez des noms de domaine chez différents registrars ? de nombreux certificats SSL Let's Encrypt en renouvellement automatique ? Vous allez apprécier de tout centraliser simplement dans une interface web : ajout d'un nom de domaine/certificat SSL en deux clics, liste récapitulative triée, barre de recherche etc."
@ -151,6 +152,10 @@ fr:
domain: Domaine domain: Domaine
domain: domain:
domain: Nom de domaine domain: Nom de domaine
unsupported: |
Cette extension n'est pas supportée automatiquement actuellement.
Vous devrez saisir et maintenir vous-même sa date d'expiration.
domain_expires_at_placeholder: AAAA-MM-JJ
ssl: ssl:
domain: Nom d'hôte domain: Nom d'hôte
notifications_hint: | notifications_hint: |

View file

@ -1,10 +1,29 @@
# Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr> # Copyright (C) 2018 Colin Darie <colin@darie.eu>, 2018 Evolix <info@evolix.fr>
# License: GNU AGPL-3+ (see full text in LICENSE file) # License: GNU AGPL-3+ (see full text in LICENSE file)
# In order to update the route map below,
# run `bundle exec annotate -r` after modifying this file
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
resources :checks, except: [:show] do
resources :notifications, only: [:destroy]
collection do
post :supports, format: :json
end
end
devise_for :users
root to: "pages#home"
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
end
# == Route Map # == Route Map
# #
# Prefix Verb URI Pattern Controller#Action # Prefix Verb URI Pattern Controller#Action
# check_notification DELETE /checks/:check_id/notifications/:id(.:format) notifications#destroy # check_notification DELETE /checks/:check_id/notifications/:id(.:format) notifications#destroy
# supports_checks POST /checks/supports(.:format) checks#supports
# checks GET /checks(.:format) checks#index # checks GET /checks(.:format) checks#index
# POST /checks(.:format) checks#create # POST /checks(.:format) checks#create
# new_check GET /checks/new(.:format) checks#new # new_check GET /checks/new(.:format) checks#new
@ -37,25 +56,10 @@
# rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show # rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
# update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update # update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
# rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create # rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
# #
# Routes for LetterOpenerWeb::Engine: # Routes for LetterOpenerWeb::Engine:
# clear_letters DELETE /clear(.:format) letter_opener_web/letters#clear # clear_letters DELETE /clear(.:format) letter_opener_web/letters#clear
# delete_letter DELETE /:id(.:format) letter_opener_web/letters#destroy # delete_letter DELETE /:id(.:format) letter_opener_web/letters#destroy
# letters GET / letter_opener_web/letters#index # letters GET / letter_opener_web/letters#index
# letter GET /:id(/:style)(.:format) letter_opener_web/letters#show # letter GET /:id(/:style)(.:format) letter_opener_web/letters#show
# GET /:id/attachments/:file(.:format) letter_opener_web/letters#attachment # GET /:id/attachments/:file(.:format) letter_opener_web/letters#attachment
# In order to update the route map above,
# run `bundle exec annotate -r` after modifying this file
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
resources :checks, except: [:show] do
resources :notifications, only: [:destroy]
end
devise_for :users
root to: "pages#home"
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
end

View file

@ -0,0 +1,5 @@
class AddModeToChecks < ActiveRecord::Migration[5.2]
def change
add_column :checks, :mode, :integer, default: 0, null: false
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2018_08_01_072038) do ActiveRecord::Schema.define(version: 2018_08_29_134404) do
create_table "check_logs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| create_table "check_logs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.bigint "check_id" t.bigint "check_id"
@ -40,6 +40,7 @@ ActiveRecord::Schema.define(version: 2018_08_01_072038) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.boolean "round_robin", default: true t.boolean "round_robin", default: true
t.integer "consecutive_failures", default: 0, null: false t.integer "consecutive_failures", default: 0, null: false
t.integer "mode", default: 0, null: false
t.index ["user_id"], name: "index_checks_on_user_id" t.index ["user_id"], name: "index_checks_on_user_id"
end end

View file

@ -8,7 +8,7 @@ if Rails.env.development?
# same name. # same name.
Annotate.set_defaults( Annotate.set_defaults(
'routes' => 'before', 'routes' => 'before',
'position_in_routes' => 'before', 'position_in_routes' => 'after',
'position_in_class' => 'before', 'position_in_class' => 'before',
'position_in_test' => 'before', 'position_in_test' => 'before',
'position_in_fixture' => 'before', 'position_in_fixture' => 'before',

14
lib/tasks/one_shot.rake Normal file
View file

@ -0,0 +1,14 @@
namespace :one_shot do
desc "Set manual mode for unsupported checks"
task reset_checks_modes: :environment do
Check.domain.find_each do |check|
check.mode = if check.supported?
:auto
else
:manual
end
check.save(validate: false)
end
end
end

View file

@ -15,6 +15,7 @@
# kind :integer not null # kind :integer not null
# last_run_at :datetime # last_run_at :datetime
# last_success_at :datetime # last_success_at :datetime
# mode :integer default("auto"), not null
# round_robin :boolean default(TRUE) # round_robin :boolean default(TRUE)
# vendor :string(255) # vendor :string(255)
# created_at :datetime not null # created_at :datetime not null
@ -44,6 +45,7 @@ FactoryBot.define do
last_run_at nil last_run_at nil
last_success_at nil last_success_at nil
consecutive_failures 0 consecutive_failures 0
mode :auto
trait :domain do trait :domain do
kind :domain kind :domain

View file

@ -24,7 +24,6 @@ class WhoisSyncJobTest < ActiveJob::TestCase
test "ignore invalid response (domain.fr)" do test "ignore invalid response (domain.fr)" do
check = create(:check, :nil_dates, domain: "domain.fr") check = create(:check, :nil_dates, domain: "domain.fr")
original_updated_at = check.updated_at
mock_system_command("whois", "domain.fr", stdout: "not a response") do mock_system_command("whois", "domain.fr", stdout: "not a response") do
WhoisSyncJob.perform_now(check.id) WhoisSyncJob.perform_now(check.id)
@ -34,7 +33,6 @@ class WhoisSyncJobTest < ActiveJob::TestCase
assert_just_now check.last_run_at assert_just_now check.last_run_at
assert_nil check.last_success_at assert_nil check.last_success_at
assert_equal original_updated_at, check.updated_at
assert check.active? assert check.active?
assert_equal 1, check.consecutive_failures assert_equal 1, check.consecutive_failures
end end
@ -62,7 +60,7 @@ class WhoisSyncJobTest < ActiveJob::TestCase
assert_equal 1, check.consecutive_failures assert_equal 1, check.consecutive_failures
end end
test "disable check when whois responds domain not found" do test "increment consecutive failures when whois responds domain not found" do
domain = "willneverexist.fr" domain = "willneverexist.fr"
check = create(:check, :nil_dates, domain: domain) check = create(:check, :nil_dates, domain: domain)
@ -72,7 +70,6 @@ class WhoisSyncJobTest < ActiveJob::TestCase
check.reload check.reload
refute check.active?
assert_just_now check.last_run_at assert_just_now check.last_run_at
assert_nil check.last_success_at assert_nil check.last_success_at
assert_equal 1, check.consecutive_failures assert_equal 1, check.consecutive_failures

View file

@ -71,4 +71,34 @@ class CheckTest < ActiveSupport::TestCase
check = build(:check, last_success_at: (10.1 * 24).hours.ago) check = build(:check, last_success_at: (10.1 * 24).hours.ago)
assert_equal 10, check.days_from_last_success assert_equal 10, check.days_from_last_success
end end
test "supported? for domain" do
check = build(:check, :domain, domain: "domain.fr")
assert check.supported?
check = build(:check, :domain, domain: "domain.cn")
refute check.supported?
# an empty domain name is still considered as supported
check = build(:check, :domain, domain: "")
assert check.supported?
end
test "supported? for SSL" do
check = build(:check, :ssl)
assert check.supported?
check = build(:check, :ssl, domain: "domain.cn")
assert check.supported?
end
test "set mode before saving" do
check = build(:check, domain: "domain.fr")
check.save!
assert check.auto?
check.domain = "domain.xyz"
check.save!
assert check.mode?
end
end end

View file

@ -108,6 +108,16 @@ class CheckProcessorTest < ActiveSupport::TestCase
assert_not_includes checks, c2 assert_not_includes checks, c2
end end
test "resolvers does not include manual checks" do
c1 = create(:check, :expires_next_week)
c2 = create(:check, :expires_next_week, domain: "fff.wxyz")
checks = @processor.resolve_expire_short_term
assert_includes checks, c1
assert_not_includes checks, c2
end
test "#sync_dates respects the interval configuration between sends" do test "#sync_dates respects the interval configuration between sends" do
create_list(:check, 3, :expires_next_week) create_list(:check, 3, :expires_next_week)

View file

@ -25,6 +25,26 @@ class ChecksTest < ApplicationSystemTestCase
fill_and_valid_new_check fill_and_valid_new_check
end end
test "create a manual domain check" do
visit new_check_path(kind: :domain)
domain = "unsupported.wxyz"
fill_in("check[domain]", with: domain)
page.find("body").click # simulate blur
fill_in("check[domain_expires_at]", with: "2022-04-05")
click_button
assert_equal checks_path, page.current_path
assert page.has_css?(".alert-success")
assert page.has_content?(domain)
check = Check.last
assert_equal Date.new(2022, 4, 5), check.domain_expires_at
end
test "create a predefined ssl check" do test "create a predefined ssl check" do
visit new_check_path(kind: :ssl) visit new_check_path(kind: :ssl)