Add age verification on sign-up (#34150)
parent
4a6cf67c46
commit
d213c585ff
|
@ -119,7 +119,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code)
|
params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code, :date_of_birth)
|
||||||
end
|
end
|
||||||
|
|
||||||
def invite
|
def invite
|
||||||
|
|
|
@ -62,7 +62,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
|
|
||||||
def configure_sign_up_params
|
def configure_sign_up_params
|
||||||
devise_parameter_sanitizer.permit(:sign_up) do |user_params|
|
devise_parameter_sanitizer.permit(:sign_up) do |user_params|
|
||||||
user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password)
|
user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password, :date_of_birth)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DateOfBirthInput < SimpleForm::Inputs::Base
|
||||||
|
OPTIONS = [
|
||||||
|
{ autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' }.freeze,
|
||||||
|
{ autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' }.freeze,
|
||||||
|
{ autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' }.freeze,
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
def input(wrapper_options = nil)
|
||||||
|
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
|
||||||
|
merged_input_options[:inputmode] = 'numeric'
|
||||||
|
|
||||||
|
values = (object.public_send(attribute_name) || '').split('.')
|
||||||
|
|
||||||
|
safe_join(Array.new(3) do |index|
|
||||||
|
options = merged_input_options.merge(OPTIONS[index]).merge id: generate_id(index), 'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{index + 1}i"), value: values[index]
|
||||||
|
@builder.text_field("#{attribute_name}(#{index + 1}i)", options)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def label_target
|
||||||
|
"#{attribute_name}_1i"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def generate_id(index)
|
||||||
|
"#{object_name}_#{attribute_name}_#{index + 1}i"
|
||||||
|
end
|
||||||
|
end
|
|
@ -353,6 +353,22 @@ code {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input.date_of_birth .label_input {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
box-sizing: content-box;
|
||||||
|
width: 32px;
|
||||||
|
flex: 0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
width: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.input.select.select--languages {
|
.input.select.select--languages {
|
||||||
min-width: 32ch;
|
min-width: 32ch;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,12 +39,14 @@ class Form::AdminSettings
|
||||||
authorized_fetch
|
authorized_fetch
|
||||||
app_icon
|
app_icon
|
||||||
favicon
|
favicon
|
||||||
|
min_age
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
INTEGER_KEYS = %i(
|
INTEGER_KEYS = %i(
|
||||||
media_cache_retention_period
|
media_cache_retention_period
|
||||||
content_cache_retention_period
|
content_cache_retention_period
|
||||||
backups_retention_period
|
backups_retention_period
|
||||||
|
min_age
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
BOOLEAN_KEYS = %i(
|
BOOLEAN_KEYS = %i(
|
||||||
|
@ -88,6 +90,7 @@ class Form::AdminSettings
|
||||||
validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) }
|
validates :show_domain_blocks, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks) }
|
||||||
validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) }
|
validates :show_domain_blocks_rationale, inclusion: { in: %w(disabled users all) }, if: -> { defined?(@show_domain_blocks_rationale) }
|
||||||
validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) }
|
validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) }
|
||||||
|
validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) }
|
||||||
validates :site_short_description, length: { maximum: DESCRIPTION_LIMIT }, if: -> { defined?(@site_short_description) }
|
validates :site_short_description, length: { maximum: DESCRIPTION_LIMIT }, if: -> { defined?(@site_short_description) }
|
||||||
validates :status_page_url, url: true, allow_blank: true
|
validates :status_page_url, url: true, allow_blank: true
|
||||||
validate :validate_site_uploads
|
validate :validate_site_uploads
|
||||||
|
|
|
@ -5,41 +5,42 @@
|
||||||
# Table name: users
|
# Table name: users
|
||||||
#
|
#
|
||||||
# id :bigint(8) not null, primary key
|
# id :bigint(8) not null, primary key
|
||||||
# email :string default(""), not null
|
# age_verified_at :datetime
|
||||||
# created_at :datetime not null
|
# approved :boolean default(TRUE), not null
|
||||||
# updated_at :datetime not null
|
# chosen_languages :string is an Array
|
||||||
# encrypted_password :string default(""), not null
|
# confirmation_sent_at :datetime
|
||||||
# reset_password_token :string
|
|
||||||
# reset_password_sent_at :datetime
|
|
||||||
# sign_in_count :integer default(0), not null
|
|
||||||
# current_sign_in_at :datetime
|
|
||||||
# last_sign_in_at :datetime
|
|
||||||
# confirmation_token :string
|
# confirmation_token :string
|
||||||
# confirmed_at :datetime
|
# confirmed_at :datetime
|
||||||
# confirmation_sent_at :datetime
|
# consumed_timestep :integer
|
||||||
# unconfirmed_email :string
|
# current_sign_in_at :datetime
|
||||||
# locale :string
|
# disabled :boolean default(FALSE), not null
|
||||||
|
# email :string default(""), not null
|
||||||
# encrypted_otp_secret :string
|
# encrypted_otp_secret :string
|
||||||
# encrypted_otp_secret_iv :string
|
# encrypted_otp_secret_iv :string
|
||||||
# encrypted_otp_secret_salt :string
|
# encrypted_otp_secret_salt :string
|
||||||
# consumed_timestep :integer
|
# encrypted_password :string default(""), not null
|
||||||
# otp_required_for_login :boolean default(FALSE), not null
|
|
||||||
# last_emailed_at :datetime
|
# last_emailed_at :datetime
|
||||||
|
# last_sign_in_at :datetime
|
||||||
|
# locale :string
|
||||||
# otp_backup_codes :string is an Array
|
# otp_backup_codes :string is an Array
|
||||||
# account_id :bigint(8) not null
|
# otp_required_for_login :boolean default(FALSE), not null
|
||||||
# disabled :boolean default(FALSE), not null
|
# otp_secret :string
|
||||||
# invite_id :bigint(8)
|
# reset_password_sent_at :datetime
|
||||||
# chosen_languages :string is an Array
|
# reset_password_token :string
|
||||||
# created_by_application_id :bigint(8)
|
# settings :text
|
||||||
# approved :boolean default(TRUE), not null
|
# sign_in_count :integer default(0), not null
|
||||||
# sign_in_token :string
|
# sign_in_token :string
|
||||||
# sign_in_token_sent_at :datetime
|
# sign_in_token_sent_at :datetime
|
||||||
# webauthn_id :string
|
|
||||||
# sign_up_ip :inet
|
# sign_up_ip :inet
|
||||||
# role_id :bigint(8)
|
|
||||||
# settings :text
|
|
||||||
# time_zone :string
|
# time_zone :string
|
||||||
# otp_secret :string
|
# unconfirmed_email :string
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint(8) not null
|
||||||
|
# created_by_application_id :bigint(8)
|
||||||
|
# invite_id :bigint(8)
|
||||||
|
# role_id :bigint(8)
|
||||||
|
# webauthn_id :string
|
||||||
#
|
#
|
||||||
|
|
||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
|
@ -111,6 +112,7 @@ class User < ApplicationRecord
|
||||||
validates_with RegistrationFormTimeValidator, on: :create
|
validates_with RegistrationFormTimeValidator, on: :create
|
||||||
validates :website, absence: true, on: :create
|
validates :website, absence: true, on: :create
|
||||||
validates :confirm_password, absence: true, on: :create
|
validates :confirm_password, absence: true, on: :create
|
||||||
|
validates :date_of_birth, presence: true, date_of_birth: true, on: :create, if: -> { Setting.min_age.present? }
|
||||||
validate :validate_role_elevation
|
validate :validate_role_elevation
|
||||||
|
|
||||||
scope :account_not_suspended, -> { joins(:account).merge(Account.without_suspended) }
|
scope :account_not_suspended, -> { joins(:account).merge(Account.without_suspended) }
|
||||||
|
@ -129,6 +131,7 @@ class User < ApplicationRecord
|
||||||
|
|
||||||
before_validation :sanitize_role
|
before_validation :sanitize_role
|
||||||
before_create :set_approved
|
before_create :set_approved
|
||||||
|
before_create :set_age_verified_at
|
||||||
after_commit :send_pending_devise_notifications
|
after_commit :send_pending_devise_notifications
|
||||||
after_create_commit :trigger_webhooks
|
after_create_commit :trigger_webhooks
|
||||||
|
|
||||||
|
@ -140,7 +143,7 @@ class User < ApplicationRecord
|
||||||
|
|
||||||
delegate :can?, to: :role
|
delegate :can?, to: :role
|
||||||
|
|
||||||
attr_reader :invite_code
|
attr_reader :invite_code, :date_of_birth
|
||||||
attr_writer :external, :bypass_invite_request_check, :current_account
|
attr_writer :external, :bypass_invite_request_check, :current_account
|
||||||
|
|
||||||
def self.those_who_can(*any_of_privileges)
|
def self.those_who_can(*any_of_privileges)
|
||||||
|
@ -157,6 +160,17 @@ class User < ApplicationRecord
|
||||||
Rails.env.local?
|
Rails.env.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def date_of_birth=(hash_or_string)
|
||||||
|
@date_of_birth = begin
|
||||||
|
if hash_or_string.is_a?(Hash)
|
||||||
|
day, month, year = hash_or_string.values_at(1, 2, 3)
|
||||||
|
"#{day}.#{month}.#{year}"
|
||||||
|
else
|
||||||
|
hash_or_string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def role
|
def role
|
||||||
if role_id.nil?
|
if role_id.nil?
|
||||||
UserRole.everyone
|
UserRole.everyone
|
||||||
|
@ -432,6 +446,10 @@ class User < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_age_verified_at
|
||||||
|
self.age_verified_at = Time.now.utc if Setting.min_age.present?
|
||||||
|
end
|
||||||
|
|
||||||
def grant_approval_on_confirmation?
|
def grant_approval_on_confirmation?
|
||||||
# Re-check approval on confirmation if the server has switched to open registrations
|
# Re-check approval on confirmation if the server has switched to open registrations
|
||||||
open_registrations? && !sign_up_from_ip_requires_approval? && !sign_up_email_requires_approval?
|
open_registrations? && !sign_up_from_ip_requires_approval? && !sign_up_email_requires_approval?
|
||||||
|
|
|
@ -107,6 +107,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
|
||||||
enabled: registrations_enabled?,
|
enabled: registrations_enabled?,
|
||||||
approval_required: Setting.registrations_mode == 'approved',
|
approval_required: Setting.registrations_mode == 'approved',
|
||||||
message: registrations_enabled? ? nil : registrations_message,
|
message: registrations_enabled? ? nil : registrations_message,
|
||||||
|
min_age: Setting.min_age.presence,
|
||||||
url: ENV.fetch('SSO_ACCOUNT_SIGN_UP', nil),
|
url: ENV.fetch('SSO_ACCOUNT_SIGN_UP', nil),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -41,7 +41,7 @@ class AppSignUpService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
@params.slice(:email, :password, :agreement, :locale, :time_zone, :invite_code)
|
@params.slice(:email, :password, :agreement, :locale, :time_zone, :invite_code, :date_of_birth)
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DateOfBirthValidator < ActiveModel::EachValidator
|
||||||
|
def validate_each(record, attribute, value)
|
||||||
|
record.errors.add(attribute, :below_limit) if value.present? && value.to_date > min_age.ago
|
||||||
|
rescue Date::Error
|
||||||
|
record.errors.add(attribute, :invalid)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def min_age
|
||||||
|
Setting.min_age.to_i.years
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,6 +12,9 @@
|
||||||
|
|
||||||
.flash-message= t('admin.settings.registrations.moderation_recommandation')
|
.flash-message= t('admin.settings.registrations.moderation_recommandation')
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= f.input :min_age, as: :string, wrapper: :with_block_label, input_html: { inputmode: 'numeric' }
|
||||||
|
|
||||||
.fields-row
|
.fields-row
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :registrations_mode,
|
= f.input :registrations_mode,
|
||||||
|
|
|
@ -21,20 +21,19 @@
|
||||||
= f.simple_fields_for :account do |ff|
|
= f.simple_fields_for :account do |ff|
|
||||||
= ff.input :username,
|
= ff.input :username,
|
||||||
append: "@#{site_hostname}",
|
append: "@#{site_hostname}",
|
||||||
input_html: { 'aria-label': t('simple_form.labels.defaults.username'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT },
|
input_html: { autocomplete: 'off', pattern: '[a-zA-Z0-9_]+', maxlength: Account::USERNAME_LENGTH_LIMIT, placeholder: ' ' },
|
||||||
label: false,
|
|
||||||
required: true,
|
required: true,
|
||||||
wrapper: :with_label
|
wrapper: :with_label
|
||||||
= f.input :email,
|
= f.input :email,
|
||||||
hint: false,
|
hint: false,
|
||||||
input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'username' },
|
input_html: { autocomplete: 'username', placeholder: ' ' },
|
||||||
placeholder: t('simple_form.labels.defaults.email'),
|
required: true,
|
||||||
required: true
|
wrapper: :with_label
|
||||||
= f.input :password,
|
= f.input :password,
|
||||||
hint: false,
|
hint: false,
|
||||||
input_html: { 'aria-label': t('simple_form.labels.defaults.password'), autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last },
|
input_html: { autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last, placeholder: ' ' },
|
||||||
placeholder: t('simple_form.labels.defaults.password'),
|
required: true,
|
||||||
required: true
|
wrapper: :with_label
|
||||||
= f.input :password_confirmation,
|
= f.input :password_confirmation,
|
||||||
hint: false,
|
hint: false,
|
||||||
input_html: { 'aria-label': t('simple_form.labels.defaults.confirm_password'), autocomplete: 'new-password', maxlength: User.password_length.last },
|
input_html: { 'aria-label': t('simple_form.labels.defaults.confirm_password'), autocomplete: 'new-password', maxlength: User.password_length.last },
|
||||||
|
@ -53,6 +52,14 @@
|
||||||
required: false,
|
required: false,
|
||||||
wrapper: :with_label
|
wrapper: :with_label
|
||||||
|
|
||||||
|
- if Setting.min_age.present?
|
||||||
|
.fields-group
|
||||||
|
= f.input :date_of_birth,
|
||||||
|
as: :date_of_birth,
|
||||||
|
hint: t('simple_form.hints.user.date_of_birth', age: Setting.min_age.to_i),
|
||||||
|
required: true,
|
||||||
|
wrapper: :with_block_label
|
||||||
|
|
||||||
- if approved_registrations? && @invite.blank?
|
- if approved_registrations? && @invite.blank?
|
||||||
%p.lead= t('auth.sign_up.manual_review', domain: site_hostname)
|
%p.lead= t('auth.sign_up.manual_review', domain: site_hostname)
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,8 @@ en:
|
||||||
too_soon: is too soon, must be later than %{date}
|
too_soon: is too soon, must be later than %{date}
|
||||||
user:
|
user:
|
||||||
attributes:
|
attributes:
|
||||||
|
date_of_birth:
|
||||||
|
below_limit: is below the age limit
|
||||||
email:
|
email:
|
||||||
blocked: uses a disallowed e-mail provider
|
blocked: uses a disallowed e-mail provider
|
||||||
unreachable: does not seem to exist
|
unreachable: does not seem to exist
|
||||||
|
|
|
@ -88,6 +88,7 @@ en:
|
||||||
favicon: WEBP, PNG, GIF or JPG. Overrides the default Mastodon favicon with a custom icon.
|
favicon: WEBP, PNG, GIF or JPG. Overrides the default Mastodon favicon with a custom icon.
|
||||||
mascot: Overrides the illustration in the advanced web interface.
|
mascot: Overrides the illustration in the advanced web interface.
|
||||||
media_cache_retention_period: Media files from posts made by remote users are cached on your server. When set to a positive value, media will be deleted after the specified number of days. If the media data is requested after it is deleted, it will be re-downloaded, if the source content is still available. Due to restrictions on how often link preview cards poll third-party sites, it is recommended to set this value to at least 14 days, or link preview cards will not be updated on demand before that time.
|
media_cache_retention_period: Media files from posts made by remote users are cached on your server. When set to a positive value, media will be deleted after the specified number of days. If the media data is requested after it is deleted, it will be re-downloaded, if the source content is still available. Due to restrictions on how often link preview cards poll third-party sites, it is recommended to set this value to at least 14 days, or link preview cards will not be updated on demand before that time.
|
||||||
|
min_age: Users will be asked to confirm their date of birth during sign-up
|
||||||
peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense.
|
peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense.
|
||||||
profile_directory: The profile directory lists all users who have opted-in to be discoverable.
|
profile_directory: The profile directory lists all users who have opted-in to be discoverable.
|
||||||
require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
|
require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
|
||||||
|
@ -146,6 +147,7 @@ en:
|
||||||
min_age: Should not be below the minimum age required by the laws of your jurisdiction.
|
min_age: Should not be below the minimum age required by the laws of your jurisdiction.
|
||||||
user:
|
user:
|
||||||
chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
|
chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
|
||||||
|
date_of_birth: We have to make sure you're at least %{age} to use Mastodon. We won't store this.
|
||||||
role: The role controls which permissions the user has.
|
role: The role controls which permissions the user has.
|
||||||
user_role:
|
user_role:
|
||||||
color: Color to be used for the role throughout the UI, as RGB in hex format
|
color: Color to be used for the role throughout the UI, as RGB in hex format
|
||||||
|
@ -271,6 +273,7 @@ en:
|
||||||
favicon: Favicon
|
favicon: Favicon
|
||||||
mascot: Custom mascot (legacy)
|
mascot: Custom mascot (legacy)
|
||||||
media_cache_retention_period: Media cache retention period
|
media_cache_retention_period: Media cache retention period
|
||||||
|
min_age: Minimum age requirement
|
||||||
peers_api_enabled: Publish list of discovered servers in the API
|
peers_api_enabled: Publish list of discovered servers in the API
|
||||||
profile_directory: Enable profile directory
|
profile_directory: Enable profile directory
|
||||||
registrations_mode: Who can sign-up
|
registrations_mode: Who can sign-up
|
||||||
|
@ -349,6 +352,9 @@ en:
|
||||||
jurisdiction: Legal jurisdiction
|
jurisdiction: Legal jurisdiction
|
||||||
min_age: Minimum age
|
min_age: Minimum age
|
||||||
user:
|
user:
|
||||||
|
date_of_birth_1i: Day
|
||||||
|
date_of_birth_2i: Month
|
||||||
|
date_of_birth_3i: Year
|
||||||
role: Role
|
role: Role
|
||||||
time_zone: Time zone
|
time_zone: Time zone
|
||||||
user_role:
|
user_role:
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddAgeVerifiedAtToUsers < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
add_column :users, :age_verified_at, :datetime
|
||||||
|
end
|
||||||
|
end
|
|
@ -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[8.0].define(version: 2025_03_05_074104) do
|
ActiveRecord::Schema[8.0].define(version: 2025_03_13_123400) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
|
|
||||||
|
@ -1188,6 +1188,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_05_074104) do
|
||||||
t.text "settings"
|
t.text "settings"
|
||||||
t.string "time_zone"
|
t.string "time_zone"
|
||||||
t.string "otp_secret"
|
t.string "otp_secret"
|
||||||
|
t.datetime "age_verified_at"
|
||||||
t.index ["account_id"], name: "index_users_on_account_id"
|
t.index ["account_id"], name: "index_users_on_account_id"
|
||||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||||
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)"
|
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)"
|
||||||
|
|
|
@ -342,6 +342,42 @@ RSpec.describe Auth::RegistrationsController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when age verification is enabled' do
|
||||||
|
subject { post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', agreement: 'true' }.merge(date_of_birth) } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Setting.min_age = 16
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:date_of_birth) { {} }
|
||||||
|
|
||||||
|
context 'when date of birth is below age limit' do
|
||||||
|
let(:date_of_birth) { 13.years.ago.then { |date| { 'date_of_birth(1i)': date.day.to_s, 'date_of_birth(2i)': date.month.to_s, 'date_of_birth(3i)': date.year.to_s } } }
|
||||||
|
|
||||||
|
it 'does not create user' do
|
||||||
|
subject
|
||||||
|
user = User.find_by(email: 'test@example.com')
|
||||||
|
expect(user).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when date of birth is above age limit' do
|
||||||
|
let(:date_of_birth) { 17.years.ago.then { |date| { 'date_of_birth(1i)': date.day.to_s, 'date_of_birth(2i)': date.month.to_s, 'date_of_birth(3i)': date.year.to_s } } }
|
||||||
|
|
||||||
|
it 'redirects to setup and creates user' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to redirect_to auth_setup_path
|
||||||
|
|
||||||
|
expect(User.find_by(email: 'test@example.com'))
|
||||||
|
.to be_present
|
||||||
|
.and have_attributes(
|
||||||
|
age_verified_at: not_eq(nil)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
include_examples 'checks for enabled registrations', :create
|
include_examples 'checks for enabled registrations', :create
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -74,12 +74,45 @@ RSpec.describe '/api/v1/accounts' do
|
||||||
|
|
||||||
describe 'POST /api/v1/accounts' do
|
describe 'POST /api/v1/accounts' do
|
||||||
subject do
|
subject do
|
||||||
post '/api/v1/accounts', headers: headers, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement }
|
post '/api/v1/accounts', headers: headers, params: { username: 'test', password: '12345678', email: 'hello@world.tld', agreement: agreement, date_of_birth: date_of_birth }
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:client_app) { Fabricate(:application) }
|
let(:client_app) { Fabricate(:application) }
|
||||||
let(:token) { Doorkeeper::AccessToken.find_or_create_for(application: client_app, resource_owner: nil, scopes: 'read write', use_refresh_token: false) }
|
let(:token) { Doorkeeper::AccessToken.find_or_create_for(application: client_app, resource_owner: nil, scopes: 'read write', use_refresh_token: false) }
|
||||||
let(:agreement) { nil }
|
let(:agreement) { nil }
|
||||||
|
let(:date_of_birth) { nil }
|
||||||
|
|
||||||
|
context 'when age verification is enabled' do
|
||||||
|
before do
|
||||||
|
Setting.min_age = 16
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:agreement) { 'true' }
|
||||||
|
|
||||||
|
context 'when date of birth is below age limit' do
|
||||||
|
let(:date_of_birth) { 13.years.ago.strftime('%d.%m.%Y') }
|
||||||
|
|
||||||
|
it 'returns http unprocessable entity' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(422)
|
||||||
|
expect(response.content_type)
|
||||||
|
.to start_with('application/json')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when date of birth is over age limit' do
|
||||||
|
let(:date_of_birth) { 17.years.ago.strftime('%d.%m.%Y') }
|
||||||
|
|
||||||
|
it 'creates a user', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(response.content_type)
|
||||||
|
.to start_with('application/json')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when given truthy agreement' do
|
context 'when given truthy agreement' do
|
||||||
let(:agreement) { 'true' }
|
let(:agreement) { 'true' }
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe DateOfBirthValidator do
|
||||||
|
let(:record_class) do
|
||||||
|
Class.new do
|
||||||
|
include ActiveModel::Validations
|
||||||
|
|
||||||
|
attr_accessor :date_of_birth
|
||||||
|
|
||||||
|
validates :date_of_birth, date_of_birth: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:record) { record_class.new }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Setting.min_age = 16
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#validate_each' do
|
||||||
|
context 'with an invalid date' do
|
||||||
|
it 'adds errors' do
|
||||||
|
record.date_of_birth = '76.830.10'
|
||||||
|
|
||||||
|
expect(record).to_not be_valid
|
||||||
|
expect(record.errors.first.attribute).to eq(:date_of_birth)
|
||||||
|
expect(record.errors.first.type).to eq(:invalid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a date below age limit' do
|
||||||
|
it 'adds errors' do
|
||||||
|
record.date_of_birth = 13.years.ago.strftime('%d.%m.%Y')
|
||||||
|
|
||||||
|
expect(record).to_not be_valid
|
||||||
|
expect(record.errors.first.attribute).to eq(:date_of_birth)
|
||||||
|
expect(record.errors.first.type).to eq(:below_limit)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a date above age limit' do
|
||||||
|
it 'does not add errors' do
|
||||||
|
record.date_of_birth = 16.years.ago.strftime('%d.%m.%Y')
|
||||||
|
|
||||||
|
expect(record).to be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue