Skip to content

Commit

Permalink
Add minors management for users (AjuntamentdeBarcelona#15)
Browse files Browse the repository at this point in the history
* update the form to allow authorizations

* update commands

* fix system test

* normalize

* add minors menu

* add user concerns and permissions

* add permissions spec

* add minor account relationship migration

* add tutor/minors relationship

* add seeds

* fix rakefile

* add more restrictions on users to be tutors/minors

* prevent minors to have minors accounts

* init architecture docs

* add tutor authorization

* normalize locales

* fix permissions

* add personal_data attribute

* add authorizations checks

* fix checking specific verification method

* refactor personal_data

* fix tests

* change workflow

* check for authorization expiration

Co-authored-by: Fran Bolívar <[email protected]>
  • Loading branch information
microstudi and fblupi authored Nov 4, 2022
1 parent 10488db commit 690898d
Show file tree
Hide file tree
Showing 28 changed files with 778 additions and 14 deletions.
24 changes: 24 additions & 0 deletions .solargraph.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
include:
- "**/*.rb"
exclude:
- spec/**/*
- test/**/*
- vendor/**/*
- node_modules/**/*
- development_app/**/*
- ".bundle/**/*"
require: []
domains: []
reporters:
- rubocop
- require_not_found
formatter:
rubocop:
cops: safe
except: []
only: []
extra_args: []
require_paths: []
plugins: []
max_files: 10000
7 changes: 7 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ def install_module(path)
end
end

def seed_db(path)
Dir.chdir(path) do
system("bundle exec rake db:seed")
end
end

desc "Generates a dummy app for testing"
task test_app: "decidim:generate_external_test_app" do
ENV["RAILS_ENV"] = "test"
Expand All @@ -30,4 +36,5 @@ task :development_app do
end

install_module("development_app")
seed_db("development_app")
end
3 changes: 3 additions & 0 deletions app/controllers/decidim/kids/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
module Decidim
module Kids
class ApplicationController < Decidim::ApplicationController
def permission_class_chain
[::Decidim::Kids::Permissions] + super
end
end
end
end
52 changes: 52 additions & 0 deletions app/controllers/decidim/kids/user_minors_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

module Decidim
module Kids
class UserMinorsController < ApplicationController
include Decidim::UserProfile

before_action do
if tutor_adapter.blank?
flash[:alert] = t("user_minors.no_tutor_authorization", scope: "decidim.kids")
redirect_to decidim.account_path
end
end

before_action except: [:unverified] do
enforce_permission_to :index, :minor_accounts
redirect_to unverified_user_minors_path unless tutor_verified?
end

helper_method :minors, :tutor_adapter

def index; end

def unverified
redirect_to user_minors_path if tutor_verified?
end

private

def minors
current_user.minors
end

def tutor_verified?
@tutor_verified ||= begin
authorization = granted_authorizations(current_user).where(name: current_organization.tutors_authorization)
authorization.any? && !authorization.first.expired?
end
end

def tutor_adapter
@tutor_adapter ||= Decidim::Verifications::Adapter.from_element(current_organization.tutors_authorization)
rescue Decidim::Verifications::UnregisteredVerificationManifest
nil
end

def granted_authorizations(user)
Decidim::Verifications::Authorizations.new(organization: current_organization, user:, granted: true).query
end
end
end
end
31 changes: 31 additions & 0 deletions app/models/concerns/decidim/kids/user_override.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module Decidim
module Kids
module UserOverride
extend ActiveSupport::Concern

included do
has_many :minor_accounts, class_name: "Decidim::Kids::MinorAccount", foreign_key: :decidim_tutor_id, dependent: :destroy
has_many :tutor_accounts, class_name: "Decidim::Kids::MinorAccount", foreign_key: :decidim_minor_id, dependent: :destroy
has_many :minors, through: :minor_accounts, class_name: "Decidim::User", foreign_key: :decidim_minor_id
has_many :tutors, through: :tutor_accounts, class_name: "Decidim::User", foreign_key: :decidim_tutor_id
has_one :minor_data,
foreign_key: "decidim_user_id",
class_name: "Decidim::Kids::MinorData",
inverse_of: :user,
dependent: :destroy

delegate :name, :email, :birthday, :extra_data, to: :minor_data, prefix: true, allow_nil: true

def minor?
tutor_accounts.exists?
end

def tutor?
minor_accounts.exists?
end
end
end
end
end
41 changes: 41 additions & 0 deletions app/models/decidim/kids/minor_account.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module Decidim
module Kids
class MinorAccount < ApplicationRecord
self.table_name = "decidim_kids_minor_accounts"

belongs_to :tutor,
foreign_key: "decidim_tutor_id",
class_name: "Decidim::User"
belongs_to :minor,
foreign_key: "decidim_minor_id",
class_name: "Decidim::User"

validate :same_organization
validate :can_be_tutor
validate :can_be_minor

private

def same_organization
return if tutor.try(:organization) == minor.try(:organization)

errors.add(:tutor, :invalid)
errors.add(:minor, :invalid)
end

def can_be_tutor
return unless tutor.minor? || !tutor.confirmed?

errors.add(:tutor, :invalid)
end

def can_be_minor
return unless minor.tutor? || minor.admin?

errors.add(:minor, :invalid)
end
end
end
end
28 changes: 28 additions & 0 deletions app/models/decidim/kids/minor_data.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Decidim
module Kids
class MinorData < ApplicationRecord
include Decidim::RecordEncryptor
self.table_name = "decidim_kids_minor_data"

belongs_to :user,
foreign_key: "decidim_user_id",
class_name: "Decidim::User"

encrypt_attribute :name, type: :string
encrypt_attribute :email, type: :string
encrypt_attribute :birthday, type: :string

validate :user_is_minor

private

def user_is_minor
return if user.minor?

errors.add(:user, :invalid)
end
end
end
end
1 change: 0 additions & 1 deletion app/models/decidim/kids/organization_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

module Decidim
module Kids
# Abstract class from which all models in this engine inherit.
class OrganizationConfig < ApplicationRecord
self.table_name = "decidim_kids_organization_configs"

Expand Down
27 changes: 27 additions & 0 deletions app/permissions/decidim/kids/permissions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Decidim
module Kids
class Permissions < Decidim::DefaultPermissions
def permissions
# return Decidim::Kids::Admin::Permissions.new(user, permission_action, context).permissions if permission_action.scope == :admin
return permission_action if permission_action.scope == :admin

return permission_action unless user

if permission_action.subject == :minor_accounts
if !user.organization.minors_participation_enabled? || user.minor? || !user.confirmed?
disallow!
else
case permission_action.action
when :index
allow!
end
end
end

permission_action
end
end
end
end
6 changes: 6 additions & 0 deletions app/views/decidim/kids/user_minors/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<% add_decidim_page_title(t("menu", scope: "decidim.kids.user")) %>
<% content_for(:subtitle) { t("menu", scope: "decidim.kids.user") } %>

list my kids

<%= minors.pluck :name %>
6 changes: 6 additions & 0 deletions app/views/decidim/kids/user_minors/unverified.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<% add_decidim_page_title(t("menu", scope: "decidim.kids.user")) %>
<% content_for(:subtitle) { t("menu", scope: "decidim.kids.user") } %>

<div class="callout warning">You need to verify your identity as a tutor to access this page</div>

verify yourself sir please with <a href="<%= tutor_adapter.root_path %>"><%= tutor_adapter.description %></a>
5 changes: 5 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ en:
checks for the age of the minor is in between the ranges defined here'
tutors_authorization_help: The user will have to be authorized using this
method before being able to create a minor account
user:
menu: My minor accounts
user_minors:
no_tutor_authorization: Minors module is misconfigured, please contact the
administrator.
13 changes: 13 additions & 0 deletions db/migrate/20221024124523_create_decidim_kids_minor_accounts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

class CreateDecidimKidsMinorAccounts < ActiveRecord::Migration[6.1]
def change
create_table :decidim_kids_minor_accounts do |t|
t.references :decidim_tutor, null: false, index: true, foreign_key: { to_table: "decidim_users" }
t.references :decidim_minor, null: false, index: true, foreign_key: { to_table: "decidim_users" }
t.timestamps
end

add_index :decidim_kids_minor_accounts, [:decidim_tutor_id, :decidim_minor_id], unique: true, name: "decidim_kids_minor_accounts_unique_tutor_and_minor_ids"
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

class CreateDecidimKidsMinorData < ActiveRecord::Migration[6.1]
def change
create_table :decidim_kids_minor_data do |t|
t.references :decidim_user, null: false, index: true
t.string :name # encrypted
t.string :birthday # encrypted
t.string :email # encrypted
t.jsonb :extra_data, null: false, default: {}
t.timestamps
end
end
end
31 changes: 31 additions & 0 deletions db/seeds.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

# create some seeds for the admin@example and the user@example
if !Rails.env.production? || ENV.fetch("SEED", nil)
print "Creating seeds for decidim_kids...\n" unless Rails.env.test?

Decidim::Kids::OrganizationConfig.create!(
organization: Decidim::Organization.first,
enable_minors_participation: true,
tutors_authorization: "dummy_authorization_handler",
minors_authorization: "dummy_authorization_handler"
)

Decidim::User.where(email: ["[email protected]", "[email protected]"]).each do |user|
2.times do
minor = Decidim::User.create!(
name: "Minor - #{Faker::Name.name}",
nickname: Faker::Twitter.unique.screen_name,
organization: user.organization,
email: Faker::Internet.email,
confirmed_at: Time.current,
locale: I18n.default_locale,
tos_agreement: true,
password: "decidim123456789",
password_confirmation: "decidim123456789",
accepted_tos_version: user.organization.tos_version + 1.hour
)
Decidim::Kids::MinorAccount.create!(minor:, tutor: user)
end
end
end
27 changes: 27 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Minors architecture

## Organization tweaks

...

## Minors an tutors definitions

1. A minor is a normal user that has a link in the table `minor_accounts` (key `decidim_minor_id`). This table links two users, a tutor and a minor.
2. Similarly, a tutor is a normal user that has a link to the table `minor_accounts` (key `decidim_tutor_id`).

## Minors account creation workflow

![](create_minor_account.png)

1. A confirmed and verified (using the system's configured workflow) user account (that is not itself a minor, called a **tutor** from now on) can create a new minor account from the profile menu page.
2. The tutor introduces the minor's personal data (name, email, birth date), this data remains encrypted in the table `minor_accounts` in a JSONB field (`minor_data`).
3. After saving the data, a new user is created in the table `decidim_users` but blocked. No personal data will be stored in this user yet, except for the email. Attributes for the model will be: `blocked: true, blocked_at: Time.current, name: "Pending minor user"`. No emails are sent at this point (no confirmation or invitation emails).
4. Tutor can now click on the "verify minor" and is redirected to the authorization controller in order to verify the minor's data. This verification handler might ask for more personal data to authorize this user.
5. Depending on the configuration of the module (see `Decidim::Kids.minor_authorization_age_attributes`), check that the age returned by the verification is in the configured range. If not, remove the verification.
6. If the verification goes through, the `Authorization` gets stored, and the user is unblocked and personal data is transferred from the table `minor_accounts->minor_data` to the user created (the minor).
7. An email is sent to the new user (aka: confirm the email), once is confirmed it can log in (maybe automatically). A notification to the tutor will be sent when the minor confirms the email.


## Emancipation workflow

...
Binary file added docs/create_minor_account.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 690898d

Please sign in to comment.