Commit 939a8900 authored by Alexis Viguié's avatar Alexis Viguié Committed by Jeffrey Phillips Freeman
Browse files

add optional registration verification with hCaptcha

parent fe117cf0
......@@ -58,3 +58,9 @@ S3_BUCKET=files.example.com
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
S3_ALIAS_HOST=files.example.com
# Optional hCaptcha verification on user signup
# If the feature is enabled and no keys are specified here, dummy keys will be used and no actual verification will be taking place.
# HCAPTCHA_ENABLED=
# HCAPTCHA_SITE_KEY=
# HCAPTCHA_SECRET_KEY=
......@@ -102,6 +102,8 @@ gem 'webpacker', '~> 5.2'
gem 'webpush'
gem 'webauthn', '~> 3.0.0.alpha1'
gem 'hcaptcha', git: 'https://github.com/firstmoversadvantage/hcaptcha', ref: 'e65246bea49374566a9dbaead59fdb35b46b086d', require: 'hcaptcha/rails'
gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.1'
gem 'rdf-normalize', '~> 0.4'
......
GIT
remote: https://github.com/firstmoversadvantage/hcaptcha
revision: e65246bea49374566a9dbaead59fdb35b46b086d
ref: e65246bea49374566a9dbaead59fdb35b46b086d
specs:
hcaptcha (6.0.1)
GIT
remote: https://github.com/ianheggie/health_check
revision: 0b799ead604f900ed50685e9b2d469cd2befba5b
......@@ -6,21 +13,6 @@ GIT
health_check (4.0.0.pre)
rails (>= 4.0)
GIT
remote: https://github.com/rtomayko/posix-spawn
revision: 58465d2e213991f8afb13b984854a49fcdcc980c
ref: 58465d2e213991f8afb13b984854a49fcdcc980c
specs:
posix-spawn (0.3.13)
GIT
remote: https://github.com/tmm1/http_parser.rb
revision: 54b17ba8c7d8d20a16dfc65d1775241833219cf2
ref: 54b17ba8c7d8d20a16dfc65d1775241833219cf2
submodules: true
specs:
http_parser.rb (0.6.1)
GIT
remote: https://github.com/witgo/nilsimsa
revision: fd184883048b922b176939f851338d0a4971a532
......@@ -755,6 +747,7 @@ DEPENDENCIES
fog-openstack (~> 0.3)
fuubar (~> 2.5)
hamlit-rails (~> 0.2)
hcaptcha!
health_check!
hiredis (~> 0.6)
htmlentities (~> 4.3)
......
......@@ -36,6 +36,7 @@ module Admin
@timeline_preview = Setting.timeline_preview
@spam_check_enabled = Setting.spam_check_enabled
@trends_enabled = Setting.trends
@hcaptcha_enabled = ENV['HCAPTCHA_ENABLED'] == 'true'
end
private
......
......@@ -20,13 +20,16 @@ class Api::V1::AccountsController < Api::BaseController
end
def create
token = AppSignUpService.new.call(doorkeeper_token.application, request.remote_ip, account_params)
response = Doorkeeper::OAuth::TokenResponse.new(token)
headers.merge!(response.headers)
self.response_body = Oj.dump(response.body)
self.status = response.status
if ENV['HCAPTCHA_ENABLED'] == 'true'
not_found
else
token = AppSignUpService.new.call(doorkeeper_token.application, request.remote_ip, account_params)
response = Doorkeeper::OAuth::TokenResponse.new(token)
headers.merge!(response.headers)
self.response_body = Oj.dump(response.body)
self.status = response.status
end
end
def follow
......
......@@ -13,6 +13,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :set_body_classes, only: [:new, :create, :edit, :update]
before_action :require_not_suspended!, only: [:update]
before_action :set_cache_headers, only: [:edit, :update]
before_action :check_captcha, only: [:create]
skip_before_action :require_functional!, only: [:edit, :update]
......@@ -98,6 +99,19 @@ class Auth::RegistrationsController < Devise::RegistrationsController
private
def check_captcha
if ENV['HCAPTCHA_ENABLED'] == 'true' && !verify_hcaptcha
build_resource(sign_up_params)
resource.validate
resource.errors.add(:base, flash.delete(:hcaptcha_error))
respond_with resource
end
end
def set_pack
use_pack %w(edit update).include?(action_name) ? 'admin' : 'auth'
end
def set_instance_presenter
@instance_presenter = InstancePresenter.new
end
......
$no-columns-breakpoint: 600px;
code {
font-family: $font-monospace, monospace;
font-weight: 400;
}
.form-container {
max-width: 400px;
padding: 20px;
margin: 0 auto;
}
.simple_form {
.input {
margin-bottom: 15px;
overflow: hidden;
&.hidden {
margin: 0;
}
&.radio_buttons {
.radio {
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
}
.radio > label {
position: relative;
padding-left: 28px;
input {
position: absolute;
top: -2px;
left: 0;
}
}
}
&.boolean {
position: relative;
margin-bottom: 0;
.label_input > label {
font-family: inherit;
font-size: 14px;
padding-top: 5px;
color: $primary-text-color;
display: block;
width: auto;
}
.label_input,
.hint {
padding-left: 28px;
}
.label_input__wrapper {
position: static;
}
label.checkbox {
position: absolute;
top: 2px;
left: 0;
}
label a {
color: $highlight-text-color;
text-decoration: underline;
&:hover,
&:active,
&:focus {
text-decoration: none;
}
}
.recommended {
position: absolute;
margin: 0 4px;
margin-top: -2px;
}
}
}
.row {
display: flex;
margin: 0 -5px;
.input {
box-sizing: border-box;
flex: 1 1 auto;
width: 50%;
padding: 0 5px;
}
}
.hint {
color: $darker-text-color;
a {
color: $highlight-text-color;
}
code {
border-radius: 3px;
padding: 0.2em 0.4em;
background: darken($ui-base-color, 12%);
}
}
span.hint {
display: block;
font-size: 12px;
margin-top: 4px;
}
p.hint {
margin-bottom: 15px;
color: $darker-text-color;
&.subtle-hint {
text-align: center;
font-size: 12px;
line-height: 18px;
margin-top: 15px;
margin-bottom: 0;
}
}
.card {
margin-bottom: 15px;
}
strong {
font-weight: 500;
@each $lang in $cjk-langs {
&:lang(#{$lang}) {
font-weight: 700;
}
}
}
.input.with_floating_label {
.label_input {
display: flex;
& > label {
font-family: inherit;
font-size: 14px;
color: $primary-text-color;
font-weight: 500;
min-width: 150px;
flex: 0 0 auto;
}
input,
select {
flex: 1 1 auto;
}
}
&.select .hint {
margin-top: 6px;
margin-left: 150px;
}
}
.input.with_label {
.label_input > label {
font-family: inherit;
font-size: 14px;
color: $primary-text-color;
display: block;
margin-bottom: 8px;
word-wrap: break-word;
font-weight: 500;
}
.hint {
margin-top: 6px;
}
ul {
flex: 390px;
}
}
.input.with_block_label {
max-width: none;
& > label {
font-family: inherit;
font-size: 16px;
color: $primary-text-color;
display: block;
font-weight: 500;
padding-top: 5px;
}
.hint {
margin-bottom: 15px;
}
ul {
columns: 2;
}
}
.input.datetime .label_input select {
display: inline-block;
width: auto;
flex: 0;
}
.required abbr {
text-decoration: none;
color: lighten($error-value-color, 12%);
}
.fields-group {
margin-bottom: 25px;
.input:last-child {
margin-bottom: 0;
}
.h-captcha {
text-align: center;
}
}
.fields-row {
display: flex;
margin: 0 -10px;
padding-top: 5px;
margin-bottom: 25px;
.input {
max-width: none;
}
&__column {
box-sizing: border-box;
padding: 0 10px;
flex: 1 1 auto;
min-height: 1px;
&-6 {
max-width: 50%;
}
.actions {
margin-top: 27px;
}
}
.fields-group:last-child,
.fields-row__column.fields-group {
margin-bottom: 0;
}
@media screen and (max-width: $no-columns-breakpoint) {
display: block;
margin-bottom: 0;
&__column {
max-width: none;
}
.fields-group:last-child,
.fields-row__column.fields-group,
.fields-row__column {
margin-bottom: 25px;
}
}
}
.input.radio_buttons .radio label {
margin-bottom: 5px;
font-family: inherit;
font-size: 14px;
color: $primary-text-color;
display: block;
width: auto;
}
.check_boxes {
.checkbox {
label {
font-family: inherit;
font-size: 14px;
color: $primary-text-color;
display: inline-block;
width: auto;
position: relative;
padding-top: 5px;
padding-left: 25px;
flex: 1 1 auto;
}
input[type=checkbox] {
position: absolute;
left: 0;
top: 5px;
margin: 0;
}
}
}
.input.static .label_input__wrapper {
font-size: 16px;
padding: 10px;
border: 1px solid $dark-text-color;
border-radius: 4px;
}
input[type=text],
input[type=number],
input[type=email],
input[type=password],
textarea {
box-sizing: border-box;
font-size: 16px;
color: $primary-text-color;
display: block;
width: 100%;
outline: 0;
font-family: inherit;
resize: vertical;
background: darken($ui-base-color, 10%);
border: 1px solid darken($ui-base-color, 14%);
border-radius: 4px;
padding: 10px;
&::placeholder {
color: lighten($darker-text-color, 4%);
}
&:invalid {
box-shadow: none;
}
&:focus:invalid:not(:placeholder-shown) {
border-color: lighten($error-red, 12%);
}
&:required:valid {
border-color: $valid-value-color;
}
&:hover {
border-color: darken($ui-base-color, 20%);
}
&:active,
&:focus {
border-color: $highlight-text-color;
background: darken($ui-base-color, 8%);
}
}
.input.field_with_errors {
label {
color: lighten($error-red, 12%);
}
input[type=text],
input[type=number],
input[type=email],
input[type=password],
textarea,
select {
border-color: lighten($error-red, 12%);
}
.error {
display: block;
font-weight: 500;
color: lighten($error-red, 12%);
margin-top: 4px;
}
}
.input.disabled {
opacity: 0.5;
}
.actions {
margin-top: 30px;
display: flex;
&.actions--top {
margin-top: 0;
margin-bottom: 30px;
}
}
button,
.button,
.block-button {
display: block;
width: 100%;
border: 0;
border-radius: 4px;
background: $ui-highlight-color;
color: $primary-text-color;
font-size: 18px;
line-height: inherit;
height: auto;
padding: 10px;
text-transform: uppercase;
text-decoration: none;
text-align: center;
box-sizing: border-box;
cursor: pointer;
font-weight: 500;
outline: 0;
margin-bottom: 10px;
margin-right: 10px;
&:last-child {
margin-right: 0;
}
&:hover {
background-color: lighten($ui-highlight-color, 5%);
}
&:active,
&:focus {
background-color: darken($ui-highlight-color, 5%);
}
&:disabled:hover {
background-color: $ui-primary-color;
}
&.negative {
background: $error-value-color;
&:hover {
background-color: lighten($error-value-color, 5%);
}
&:active,
&:focus {
background-color: darken($error-value-color, 5%);
}
}
}
select {
appearance: none;
box-sizing: border-box;
font-size: 16px;
color: $primary-text-color;
display: block;
width: 100%;
outline: 0;
font-family: inherit;
resize: vertical;
background: darken($ui-base-color, 10%) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 12%))}'/></svg>") no-repeat right 8px center / auto 16px;
border: 1px solid darken($ui-base-color, 14%);
border-radius: 4px;
padding-left: 10px;
padding-right: 30px;
height: 41px;
}
h4 {
margin-bottom: 15px !important;
}
.label_input {
&__wrapper {
position: relative;
}
&__append {
position: absolute;
right: 3px;
top: 1px;
padding: 10px;
padding-bottom: 9px;