diff --git a/.editorconfig b/.editorconfig
index 16ff5720c..111c6ec90 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -10,5 +10,5 @@ indent_size = 2
indent_style = space
trim_trailing_whitespace = true
-[*.{ico,keep,pdf,svg}]
+[*.{ico,keep,pdf,svg,der}]
insert_final_newline = false
diff --git a/.rubocop.yml b/.rubocop.yml
index 307bc088d..445c3400a 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -726,7 +726,7 @@ Rails/HttpStatus:
Enabled: false
Rails/InverseOf:
- Enabled: false
+ Enabled: true
Rails/LexicallyScopedActionFilter:
Enabled: false
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6a51ae6d8..295a988ae 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -45,8 +45,6 @@ demarches-simplifiees.fr est **compliqué à héberger**. Parmi les problématiq
Si vous souhaitez adapter demarches-simplifiees.fr à votre besoin, nous vous recommandons de **proposer vos modifications à la base de code principale** (par exemple en créant une issue) **plutôt que d’héberger une autre instance vous-même**.
-Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d'assurer de support technique concernant votre installation.
+Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d’assurer de support technique concernant votre installation.
-Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d'assurer de support technique concernant votre installation.
-
-Totefois, le ministère des armées a déployé une instance au sein de leur intranet. Nous proposons aux acteurs qui sont interessés de les mettre en relation avec eux afin de disposer d'un retour d'expérience, et bénéficier de leur retour.
+Toutefois, certains acteurs (le ministère des armées, l’administration autonome en Polynésie française) ont déployé des instances séparées. Nous proposons aux personnes intéressées de les mettre en relation avec ces acteurs existants, afin de disposer d’un retour d’expérience, et de bénéficier de leur retour.
diff --git a/README.md b/README.md
index e8267b183..f113289d4 100644
--- a/README.md
+++ b/README.md
@@ -70,6 +70,7 @@ En local, un utilisateur de test est créé automatiquement, avec les identifian
WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later
GestionnaireEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later
PurgeUnattachedBlobsJob.set(cron: "0 0 * * *").perform_later
+ OperationsSignatureJob.set(cron: "0 6 * * *").perform_later
### Voir les emails envoyés en local
diff --git a/app/controllers/manager/bill_signatures_controller.rb b/app/controllers/manager/bill_signatures_controller.rb
new file mode 100644
index 000000000..8e454c083
--- /dev/null
+++ b/app/controllers/manager/bill_signatures_controller.rb
@@ -0,0 +1,4 @@
+module Manager
+ class BillSignaturesController < Manager::ApplicationController
+ end
+end
diff --git a/app/dashboards/bill_signature_dashboard.rb b/app/dashboards/bill_signature_dashboard.rb
new file mode 100644
index 000000000..91e5d7d1f
--- /dev/null
+++ b/app/dashboards/bill_signature_dashboard.rb
@@ -0,0 +1,22 @@
+require "administrate/base_dashboard"
+
+class BillSignatureDashboard < Administrate::BaseDashboard
+ ATTRIBUTE_TYPES = {
+ dossier_operation_logs: Field::HasMany,
+ id: Field::Number,
+ digest: Field::String,
+ created_at: Field::DateTime,
+ updated_at: Field::DateTime,
+ serialized: AttachmentField,
+ signature: AttachmentField
+ }.freeze
+
+ COLLECTION_ATTRIBUTES = [
+ :id,
+ :created_at,
+ :dossier_operation_logs,
+ :digest,
+ :serialized,
+ :signature
+ ].freeze
+end
diff --git a/app/fields/attachment_field.rb b/app/fields/attachment_field.rb
new file mode 100644
index 000000000..2022fb355
--- /dev/null
+++ b/app/fields/attachment_field.rb
@@ -0,0 +1,11 @@
+require "administrate/field/base"
+
+class AttachmentField < Administrate::Field::Base
+ def to_s
+ data.filename.to_s
+ end
+
+ def blob_path
+ Rails.application.routes.url_helpers.rails_blob_path(data)
+ end
+end
diff --git a/app/jobs/operations_signature_job.rb b/app/jobs/operations_signature_job.rb
new file mode 100644
index 000000000..092aec740
--- /dev/null
+++ b/app/jobs/operations_signature_job.rb
@@ -0,0 +1,15 @@
+class OperationsSignatureJob < ApplicationJob
+ queue_as :cron
+
+ def perform(*args)
+ last_midnight = Time.zone.today.beginning_of_day
+ operations_by_day = BillSignatureService.grouped_unsigned_operation_until(last_midnight)
+ operations_by_day.each do |day, operations|
+ begin
+ BillSignatureService.sign_operations(operations, day)
+ rescue
+ raise # let errors show up on Sentry and delayed_jobs
+ end
+ end
+ end
+end
diff --git a/app/lib/asn1/timestamp.rb b/app/lib/asn1/timestamp.rb
new file mode 100644
index 000000000..6dd65e4db
--- /dev/null
+++ b/app/lib/asn1/timestamp.rb
@@ -0,0 +1,25 @@
+class ASN1::Timestamp
+ ## Poor man’s rfc3161 timestamp decoding
+ # This works, as of 2019-05, for timestamps delivered by the universign POST api.
+ # We should properly access the ASN1 contents using the sequence and tags structure.
+ # However:
+ # * It’s hard to do right.
+ # * We currently don’t require it for proper operation; timestamps are never exposed to users.
+ # * There’s an ongoing PR https://github.com/ruby/openssl/pull/204 for proper timestamp decoding in the ruby openssl library; let’s use OpenSSL::TS once it exists.
+
+ def self.timestampInfo(asn1timestamp)
+ asn1 = OpenSSL::ASN1.decode(asn1timestamp)
+ tstInfo = OpenSSL::ASN1.decode(asn1.value[1].value[0].value[2].value[1].value[0].value)
+ tstInfo
+ end
+
+ def self.signature_time(asn1timestamp)
+ tstInfo = timestampInfo(asn1timestamp)
+ tstInfo.value[4].value
+ end
+
+ def self.signed_digest(asn1timestamp)
+ tstInfo = timestampInfo(asn1timestamp)
+ tstInfo.value[2].value[1].value.unpack1('H*')
+ end
+end
diff --git a/app/lib/universign/api.rb b/app/lib/universign/api.rb
new file mode 100644
index 000000000..eeb987ca1
--- /dev/null
+++ b/app/lib/universign/api.rb
@@ -0,0 +1,40 @@
+class Universign::API
+ ## Universign Timestamp POST API
+ # Official documentation is at https://help.universign.com/hc/fr/articles/360000898965-Guide-d-intégration-horodatage
+
+ def self.ensure_properly_configured!
+ if userpwd.blank?
+ raise StandardError, 'Universign API is not properly configured'
+ end
+ end
+
+ def self.timestamp(data)
+ ensure_properly_configured!
+
+ response = Typhoeus.post(
+ UNIVERSIGN_API_URL,
+ userpwd: userpwd,
+ body: body(data)
+ )
+
+ if response.success?
+ response.body
+ else
+ raise StandardError, "Universign timestamp query failed: #{response.status_message}"
+ end
+ end
+
+ private
+
+ def self.body(data)
+ {
+ 'hashAlgo': 'SHA256',
+ 'withCert': 'true',
+ 'hashValue': data
+ }
+ end
+
+ def self.userpwd
+ Rails.application.secrets.universign[:userpwd]
+ end
+end
diff --git a/app/models/avis.rb b/app/models/avis.rb
index 7203870ea..251b80647 100644
--- a/app/models/avis.rb
+++ b/app/models/avis.rb
@@ -1,7 +1,7 @@
class Avis < ApplicationRecord
include EmailSanitizableConcern
- belongs_to :dossier, touch: true
+ belongs_to :dossier, inverse_of: :avis, touch: true
belongs_to :gestionnaire
belongs_to :claimant, class_name: 'Gestionnaire'
diff --git a/app/models/bill_signature.rb b/app/models/bill_signature.rb
new file mode 100644
index 000000000..cef20c232
--- /dev/null
+++ b/app/models/bill_signature.rb
@@ -0,0 +1,84 @@
+class BillSignature < ApplicationRecord
+ has_many :dossier_operation_logs
+
+ has_one_attached :serialized
+ has_one_attached :signature
+
+ validate :check_bill_digest
+ validate :check_serialized_bill_contents
+ validate :check_signature_contents
+
+ def self.build_with_operations(operations, day)
+ bill = new(dossier_operation_logs: operations)
+
+ bill.serialize_operations(day)
+
+ bill
+ end
+
+ def serialize_operations(day)
+ self.serialized.attach(
+ io: StringIO.new(operations_bill_json),
+ filename: "demarches-simplifiees-operations-#{day.to_date.iso8601}.json",
+ content_type: 'application/json',
+ metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
+ )
+
+ self.digest = operations_bill_digest
+ end
+
+ def operations_bill
+ dossier_operation_logs.map { |op| [op.id.to_s, op.digest] }.to_h
+ end
+
+ def operations_bill_json
+ operations_bill.to_json
+ end
+
+ def operations_bill_digest
+ Digest::SHA256.hexdigest(operations_bill_json)
+ end
+
+ def set_signature(signature, day)
+ self.signature.attach(
+ io: StringIO.new(signature),
+ filename: "demarches-simplifiees-signature-#{day.to_date.iso8601}.der",
+ content_type: 'application/x-x509-ca-cert'
+ )
+ end
+
+ # Validations
+ def check_bill_digest
+ if self.digest != self.operations_bill_digest
+ errors.add(:digest)
+ end
+ end
+
+ def check_serialized_bill_contents
+ if !self.serialized.attached?
+ errors.add(:serialized, :blank)
+ return
+ end
+
+ if JSON.parse(self.serialized.download) != self.operations_bill
+ errors.add(:serialized)
+ end
+ end
+
+ def check_signature_contents
+ if !self.signature.attached?
+ errors.add(:signature, :blank)
+ return
+ end
+
+ timestamp_signature_date = ASN1::Timestamp.signature_time(self.signature.download)
+ if timestamp_signature_date > Time.zone.now
+ errors.add(:signature, :invalid_date)
+ end
+
+ timestamp_signed_digest = ASN1::Timestamp.signed_digest(self.signature.download)
+ if timestamp_signed_digest != self.digest
+ errors.add(:signature)
+ end
+ end
+end
diff --git a/app/models/champ.rb b/app/models/champ.rb
index 06bc65871..91835499f 100644
--- a/app/models/champ.rb
+++ b/app/models/champ.rb
@@ -1,5 +1,5 @@
class Champ < ApplicationRecord
- belongs_to :dossier, touch: true
+ belongs_to :dossier, inverse_of: :champs, touch: true
belongs_to :type_de_champ, inverse_of: :champ
belongs_to :parent, class_name: 'Champ'
has_many :commentaires
diff --git a/app/models/champs/repetition_champ.rb b/app/models/champs/repetition_champ.rb
index 7f8313e16..50295a6f6 100644
--- a/app/models/champs/repetition_champ.rb
+++ b/app/models/champs/repetition_champ.rb
@@ -1,5 +1,5 @@
class Champs::RepetitionChamp < Champ
- has_many :champs, -> { ordered }, foreign_key: :parent_id, dependent: :destroy
+ has_many :champs, -> { ordered }, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
accepts_nested_attributes_for :champs, allow_destroy: true
diff --git a/app/models/commentaire.rb b/app/models/commentaire.rb
index 5a49b6e84..68b571164 100644
--- a/app/models/commentaire.rb
+++ b/app/models/commentaire.rb
@@ -1,5 +1,5 @@
class Commentaire < ApplicationRecord
- belongs_to :dossier, touch: true
+ belongs_to :dossier, inverse_of: :commentaires, touch: true
belongs_to :piece_justificative
belongs_to :user
diff --git a/app/models/dossier.rb b/app/models/dossier.rb
index 3bd702d47..3449517ec 100644
--- a/app/models/dossier.rb
+++ b/app/models/dossier.rb
@@ -19,18 +19,18 @@ class Dossier < ApplicationRecord
has_one :individual, dependent: :destroy
has_one :attestation, dependent: :destroy
- has_many :pieces_justificatives, dependent: :destroy
+ has_many :pieces_justificatives, inverse_of: :dossier, dependent: :destroy
has_one_attached :justificatif_motivation
- has_many :champs, -> { root.public_only.ordered }, dependent: :destroy
- has_many :champs_private, -> { root.private_only.ordered }, class_name: 'Champ', dependent: :destroy
- has_many :commentaires, dependent: :destroy
+ has_many :champs, -> { root.public_only.ordered }, inverse_of: :dossier, dependent: :destroy
+ has_many :champs_private, -> { root.private_only.ordered }, class_name: 'Champ', inverse_of: :dossier, dependent: :destroy
+ has_many :commentaires, inverse_of: :dossier, dependent: :destroy
has_many :invites, dependent: :destroy
- has_many :follows, -> { active }
- has_many :previous_follows, -> { inactive }, class_name: 'Follow'
+ has_many :follows, -> { active }, inverse_of: :dossier
+ has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :dossier
has_many :followers_gestionnaires, through: :follows, source: :gestionnaire
has_many :previous_followers_gestionnaires, -> { distinct }, through: :previous_follows, source: :gestionnaire
- has_many :avis, dependent: :destroy
+ has_many :avis, inverse_of: :dossier, dependent: :destroy
has_many :dossier_operation_logs, dependent: :destroy
diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb
index e517dcb36..a66492ace 100644
--- a/app/models/dossier_operation_log.rb
+++ b/app/models/dossier_operation_log.rb
@@ -12,6 +12,7 @@ class DossierOperationLog < ApplicationRecord
belongs_to :dossier
has_one_attached :serialized
+ belongs_to :bill_signature, optional: true
def self.create_and_serialize(params)
dossier = params.fetch(:dossier)
diff --git a/app/models/gestionnaire.rb b/app/models/gestionnaire.rb
index 24a7b5498..c5ab8b7c2 100644
--- a/app/models/gestionnaire.rb
+++ b/app/models/gestionnaire.rb
@@ -12,12 +12,12 @@ class Gestionnaire < ApplicationRecord
has_many :assign_to, dependent: :destroy
has_many :procedures, through: :assign_to
- has_many :assign_to_with_email_notifications, -> { with_email_notifications }, class_name: 'AssignTo'
+ has_many :assign_to_with_email_notifications, -> { with_email_notifications }, class_name: 'AssignTo', inverse_of: :gestionnaire
has_many :procedures_with_email_notifications, through: :assign_to_with_email_notifications, source: :procedure
has_many :dossiers, -> { state_not_brouillon }, through: :procedures
- has_many :follows, -> { active }
- has_many :previous_follows, -> { inactive }, class_name: 'Follow'
+ has_many :follows, -> { active }, inverse_of: :gestionnaire
+ has_many :previous_follows, -> { inactive }, class_name: 'Follow', inverse_of: :gestionnaire
has_many :followed_dossiers, through: :follows, source: :dossier
has_many :previously_followed_dossiers, -> { distinct }, through: :previous_follows, source: :dossier
has_many :avis
diff --git a/app/models/piece_justificative.rb b/app/models/piece_justificative.rb
index 20a95260c..48e4876fc 100644
--- a/app/models/piece_justificative.rb
+++ b/app/models/piece_justificative.rb
@@ -1,5 +1,5 @@
class PieceJustificative < ApplicationRecord
- belongs_to :dossier, touch: true
+ belongs_to :dossier, inverse_of: :pieces_justificatives, touch: true
belongs_to :type_de_piece_justificative
has_one :commentaire
diff --git a/app/models/procedure.rb b/app/models/procedure.rb
index 0cfe9a71d..c1eddeba0 100644
--- a/app/models/procedure.rb
+++ b/app/models/procedure.rb
@@ -5,9 +5,9 @@ class Procedure < ApplicationRecord
MAX_DUREE_CONSERVATION = 36
- has_many :types_de_piece_justificative, -> { ordered }, dependent: :destroy
- has_many :types_de_champ, -> { root.public_only.ordered }, dependent: :destroy
- has_many :types_de_champ_private, -> { root.private_only.ordered }, class_name: 'TypeDeChamp', dependent: :destroy
+ has_many :types_de_piece_justificative, -> { ordered }, inverse_of: :procedure, dependent: :destroy
+ has_many :types_de_champ, -> { root.public_only.ordered }, inverse_of: :procedure, dependent: :destroy
+ has_many :types_de_champ_private, -> { root.private_only.ordered }, class_name: 'TypeDeChamp', inverse_of: :procedure, dependent: :destroy
has_many :dossiers, dependent: :restrict_with_exception
has_many :deleted_dossiers, dependent: :destroy
diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb
index f3b9efe86..924672ec9 100644
--- a/app/models/type_de_champ.rb
+++ b/app/models/type_de_champ.rb
@@ -32,7 +32,7 @@ class TypeDeChamp < ApplicationRecord
belongs_to :procedure
belongs_to :parent, class_name: 'TypeDeChamp'
- has_many :types_de_champ, -> { ordered }, foreign_key: :parent_id, class_name: 'TypeDeChamp', dependent: :destroy
+ has_many :types_de_champ, -> { ordered }, foreign_key: :parent_id, class_name: 'TypeDeChamp', inverse_of: :parent, dependent: :destroy
store_accessor :options, :cadastres, :quartiers_prioritaires, :parcelles_agricoles, :old_pj
delegate :tags_for_template, to: :dynamic_type
diff --git a/app/services/bill_signature_service.rb b/app/services/bill_signature_service.rb
new file mode 100644
index 000000000..f8640a4b1
--- /dev/null
+++ b/app/services/bill_signature_service.rb
@@ -0,0 +1,16 @@
+class BillSignatureService
+ def self.grouped_unsigned_operation_until(date)
+ unsigned_operations = DossierOperationLog
+ .where(bill_signature: nil)
+ .where('executed_at < ?', date)
+
+ unsigned_operations.group_by { |e| e.executed_at.to_date }
+ end
+
+ def self.sign_operations(operations, day)
+ bill = BillSignature.build_with_operations(operations, day)
+ signature = Universign::API.timestamp(bill.digest)
+ bill.set_signature(signature, day)
+ bill.save!
+ end
+end
diff --git a/app/views/fields/attachment_field/_index.html.haml b/app/views/fields/attachment_field/_index.html.haml
new file mode 100644
index 000000000..d29687a35
--- /dev/null
+++ b/app/views/fields/attachment_field/_index.html.haml
@@ -0,0 +1 @@
+= link_to(field.to_s, field.blob_path)
diff --git a/app/views/manager/bill_signatures/_collection.html.erb b/app/views/manager/bill_signatures/_collection.html.erb
new file mode 100644
index 000000000..aea9f28c7
--- /dev/null
+++ b/app/views/manager/bill_signatures/_collection.html.erb
@@ -0,0 +1,68 @@
+<%#
+# Collection
+
+This partial is used on the `index` and `show` pages
+to display a collection of resources in an HTML table.
+
+## Local variables:
+
+- `collection_presenter`:
+ An instance of [Administrate::Page::Collection][1].
+ The table presenter uses `ResourceDashboard::COLLECTION_ATTRIBUTES` to determine
+ the columns displayed in the table
+- `resources`:
+ An ActiveModel::Relation collection of resources to be displayed in the table.
+ By default, the number of resources is limited by pagination
+ or by a hard limit to prevent excessive page load times
+
+[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Collection
+%>
+
+
+
+
+ <% collection_presenter.attribute_types.each do |attr_name, attr_type| %>
+
+ <%= link_to(sanitized_order_params(page, collection_field_name).merge(
+ collection_presenter.order_params_for(attr_name, key: collection_field_name)
+ )) do %>
+ <%= t(
+ "helpers.label.#{collection_presenter.resource_name}.#{attr_name}",
+ default: attr_name.to_s,
+ ).titleize %>
+ <% if collection_presenter.ordered_by?(attr_name) %>
+
+
+
+ <% end %>
+ <% end %>
+ |
+ <% end %>
+ <% [valid_action?(:edit, collection_presenter.resource_name),
+ valid_action?(:destroy, collection_presenter.resource_name)].count(true).times do %>
+ |
+ <% end %>
+
+
+
+
+ <% resources.each do |resource| %>
+
+ <% collection_presenter.attributes_for(resource).each do |attribute| %>
+
+ <%= render_field attribute %>
+ |
+ <% end %>
+
+ <% end %>
+
+
diff --git a/config/env.example b/config/env.example
index b1aa41f94..84fd7aeb7 100644
--- a/config/env.example
+++ b/config/env.example
@@ -66,3 +66,6 @@ TRUSTED_NETWORKS=""
SKYLIGHT_AUTHENTICATION_KEY=""
LOGRAGE_ENABLED="disabled"
+
+UNIVERSIGN_API_URL=""
+UNIVERSIGN_USERPWD=""
diff --git a/config/initializers/urls.rb b/config/initializers/urls.rb
index 92a27a505..585f11740 100644
--- a/config/initializers/urls.rb
+++ b/config/initializers/urls.rb
@@ -7,6 +7,7 @@ API_GEO_SANDBOX_URL = ENV.fetch("API_GEO_SANDBOX_URL", "https://sandbox.geo.api.
HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2")
PIPEDRIVE_API_URL = ENV.fetch("PIPEDRIVE_API_URL", "https://api.pipedrive.com/v1")
SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2")
+UNIVERSIGN_API_URL = ENV.fetch("UNIVERSIGN_API_URL", "https://ws.universign.eu/tsa/post/")
# Internal URLs
FOG_BASE_URL = "https://static.demarches-simplifiees.fr"
diff --git a/config/locales/models/bill_signature/fr.yml b/config/locales/models/bill_signature/fr.yml
new file mode 100644
index 000000000..b614025fe
--- /dev/null
+++ b/config/locales/models/bill_signature/fr.yml
@@ -0,0 +1,34 @@
+fr:
+ activerecord:
+ attributes:
+ bill_signature:
+ dossier_operation_logs:
+ one: opération
+ other: opérations
+ digest: empreinte
+ serialized: liasse
+ signature: signature
+ errors:
+ models:
+ bill_signature:
+ attributes:
+ digest:
+ invalid: 'ne correspond pas à la liasse'
+ serialized:
+ blank: 'doit être rempli'
+ invalid: 'ne correspond pas aux opérations'
+ signature:
+ blank: 'doit être rempli'
+ invalid: 'ne correspond pas à l’empreinte'
+ invalid_date: 'ne doit pas être dans le futur'
+ models:
+ bill_signature:
+ one: Horodatage
+ other: Horodatages
+ helpers:
+ label:
+ bill_signature:
+ dossier_operation_logs: opérations
+ digest: empreinte
+ serialized: liasse
+ signature: signature
diff --git a/config/routes.rb b/config/routes.rb
index da1c66ecc..e6006507c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -37,6 +37,8 @@ Rails.application.routes.draw do
resources :demandes, only: [:index]
+ resources :bill_signatures, only: [:index]
+
resources :services, only: [:index, :show]
post 'demandes/create_administrateur'
diff --git a/config/secrets.yml b/config/secrets.yml
index 9a74a71cc..7f3166b96 100644
--- a/config/secrets.yml
+++ b/config/secrets.yml
@@ -64,6 +64,8 @@ defaults: &defaults
crisp:
enabled: <%= ENV['CRISP_ENABLED'] == 'enabled' %>
client_key: <%= ENV['CRISP_CLIENT_KEY'] %>
+ universign:
+ userpwd: <%= ENV['UNIVERSIGN_USERPWD'] %>
@@ -90,6 +92,8 @@ test:
token_endpoint: https://bidon.com/endpoint
userinfo_endpoint: https://bidon.com/endpoint
logout_endpoint: https://bidon.com/endpoint
+ universign:
+ userpwd: 'fake:fake'
# Do not keep production secrets in the repository,
# instead read values from the environment.
diff --git a/db/migrate/20190616141702_create_bill_signature.rb b/db/migrate/20190616141702_create_bill_signature.rb
new file mode 100644
index 000000000..95b0b9a2e
--- /dev/null
+++ b/db/migrate/20190616141702_create_bill_signature.rb
@@ -0,0 +1,10 @@
+class CreateBillSignature < ActiveRecord::Migration[5.2]
+ def change
+ create_table :bill_signatures do |t|
+ t.string :digest
+ t.timestamps
+ end
+
+ add_reference :dossier_operation_logs, :bill_signature, foreign_key: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0a29eae67..f44e8df90 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_06_07_124156) do
+ActiveRecord::Schema.define(version: 2019_06_16_141702) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -145,6 +145,12 @@ ActiveRecord::Schema.define(version: 2019_06_07_124156) do
t.index ["gestionnaire_id"], name: "index_avis_on_gestionnaire_id"
end
+ create_table "bill_signatures", force: :cascade do |t|
+ t.string "digest"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "champs", id: :serial, force: :cascade do |t|
t.string "value"
t.integer "type_de_champ_id"
@@ -224,7 +230,9 @@ ActiveRecord::Schema.define(version: 2019_06_07_124156) do
t.datetime "keep_until"
t.datetime "executed_at"
t.text "digest"
+ t.bigint "bill_signature_id"
t.index ["administration_id"], name: "index_dossier_operation_logs_on_administration_id"
+ t.index ["bill_signature_id"], name: "index_dossier_operation_logs_on_bill_signature_id"
t.index ["dossier_id"], name: "index_dossier_operation_logs_on_dossier_id"
t.index ["gestionnaire_id"], name: "index_dossier_operation_logs_on_gestionnaire_id"
t.index ["keep_until"], name: "index_dossier_operation_logs_on_keep_until"
@@ -617,6 +625,7 @@ ActiveRecord::Schema.define(version: 2019_06_07_124156) do
add_foreign_key "closed_mails", "procedures"
add_foreign_key "commentaires", "dossiers"
add_foreign_key "dossier_operation_logs", "administrations"
+ add_foreign_key "dossier_operation_logs", "bill_signatures"
add_foreign_key "dossier_operation_logs", "dossiers"
add_foreign_key "dossier_operation_logs", "gestionnaires"
add_foreign_key "dossiers", "users"
diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb
index 9d68d2003..1da3997af 100644
--- a/spec/controllers/users/dossiers_controller_spec.rb
+++ b/spec/controllers/users/dossiers_controller_spec.rb
@@ -386,9 +386,11 @@ describe Users::DossiersController, type: :controller do
describe '#update_brouillon' do
before { sign_in(user) }
+
let!(:dossier) { create(:dossier, user: user) }
let(:first_champ) { dossier.champs.first }
let(:value) { 'beautiful value' }
+ let(:now) { Time.zone.parse('01/01/2100') }
let(:submit_payload) do
{
id: dossier.id,
@@ -402,7 +404,11 @@ describe Users::DossiersController, type: :controller do
end
let(:payload) { submit_payload }
- subject { patch :update_brouillon, params: payload }
+ subject do
+ Timecop.freeze(now) do
+ patch :update_brouillon, params: payload
+ end
+ end
context 'when the dossier cannot be updated by the user' do
let!(:dossier) { create(:dossier, :en_instruction, user: user) }
@@ -421,6 +427,7 @@ describe Users::DossiersController, type: :controller do
expect(response).to redirect_to(merci_dossier_path(dossier))
expect(first_champ.reload.value).to eq('beautiful value')
+ expect(dossier.reload.updated_at.year).to eq(2100)
expect(dossier.reload.state).to eq(Dossier.states.fetch(:en_construction))
end
@@ -549,9 +556,15 @@ describe Users::DossiersController, type: :controller do
describe '#update' do
before { sign_in(user) }
- let!(:dossier) { create(:dossier, :en_construction, user: user) }
+
+ let(:procedure) { create(:procedure, :published, :with_type_de_champ, :with_piece_justificative) }
+ let!(:dossier) { create(:dossier, :en_construction, user: user, procedure: procedure) }
let(:first_champ) { dossier.champs.first }
+ let(:piece_justificative_champ) { dossier.champs.last }
let(:value) { 'beautiful value' }
+ let(:file) { Rack::Test::UploadedFile.new("./spec/fixtures/files/piece_justificative_0.pdf", 'application/pdf') }
+ let(:now) { Time.zone.parse('01/01/2100') }
+
let(:submit_payload) do
{
id: dossier.id,
@@ -565,7 +578,11 @@ describe Users::DossiersController, type: :controller do
end
let(:payload) { submit_payload }
- subject { patch :update, params: payload }
+ subject do
+ Timecop.freeze(now) do
+ patch :update, params: payload
+ end
+ end
context 'when the dossier cannot be updated by the user' do
let!(:dossier) { create(:dossier, :en_instruction, user: user) }
@@ -584,8 +601,28 @@ describe Users::DossiersController, type: :controller do
expect(response).to redirect_to(demande_dossier_path(dossier))
expect(first_champ.reload.value).to eq('beautiful value')
+ expect(dossier.reload.updated_at.year).to eq(2100)
expect(dossier.reload.state).to eq(Dossier.states.fetch(:en_construction))
end
+
+ context 'when only files champs are modified' do
+ let(:submit_payload) do
+ {
+ id: dossier.id,
+ dossier: {
+ champs_attributes: {
+ id: piece_justificative_champ.id,
+ piece_justificative_file: file
+ }
+ }
+ }
+ end
+
+ it 'updates the dossier modification date' do
+ subject
+ expect(dossier.reload.updated_at.year).to eq(2100)
+ end
+ end
end
context 'when the update fails' do
diff --git a/spec/factories/bill_signature.rb b/spec/factories/bill_signature.rb
new file mode 100644
index 000000000..363aafb8f
--- /dev/null
+++ b/spec/factories/bill_signature.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :bill_signature do
+ serialized { Rack::Test::UploadedFile.new("./spec/fixtures/files/bill_signature/serialized.json", 'application/json') }
+ signature { Rack::Test::UploadedFile.new("./spec/fixtures/files/bill_signature/signature.der", 'application/x-x509-ca-cert') }
+ end
+end
diff --git a/spec/factories/dossier_operation_log.rb b/spec/factories/dossier_operation_log.rb
new file mode 100644
index 000000000..ff6a80a53
--- /dev/null
+++ b/spec/factories/dossier_operation_log.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :dossier_operation_log do
+ operation { :passer_en_instruction }
+ end
+end
diff --git a/spec/factories/procedure.rb b/spec/factories/procedure.rb
index 98488e884..8c62169b3 100644
--- a/spec/factories/procedure.rb
+++ b/spec/factories/procedure.rb
@@ -127,6 +127,14 @@ FactoryBot.define do
end
end
+ trait :with_piece_justificative do
+ after(:build) do |procedure, _evaluator|
+ type_de_champ = create(:type_de_champ_piece_justificative)
+ procedure.types_de_champ << type_de_champ
+ end
+ end
+
+ # Deprecated
trait :with_two_type_de_piece_justificative do
after(:build) do |procedure, _evaluator|
rib = create(:type_de_piece_justificative, :rib, order_place: 1)
diff --git a/spec/fixtures/cassettes/universign.yml b/spec/fixtures/cassettes/universign.yml
new file mode 100644
index 000000000..3f84c27d9
--- /dev/null
+++ b/spec/fixtures/cassettes/universign.yml
@@ -0,0 +1,43 @@
+---
+http_interactions:
+- request:
+ method: post
+ uri: https://ws.universign.eu/tsa/post/
+ body:
+ encoding: UTF-8
+ string: hashAlgo=SHA256&hashValue=d28d6c7742c6c77025a104b81750ce8dc5f7dba2d01d9b5f4cad828f02b2324b&withCert=true
+ headers:
+ User-Agent:
+ - demarches-simplifiees.fr
+ Expect:
+ - ''
+ Authorization:
+ - Basic cmllbiBpY2kK=
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Date:
+ - Fri, 17 May 2019 10:05:12 GMT
+ Server:
+ - Apache
+ Cache-Control:
+ - no-cache
+ Pragma:
+ - no-cache
+ Content-Type:
+ - application/octet-stream
+ Set-Cookie:
+ - CGSESSIONID=A3D72C1B0D07C073CFE68597B3FBF1B6E639C351;Path=/;Version=0
+ X-Ua-Compatible:
+ - IE=Edge
+ Transfer-Encoding:
+ - chunked
+ body:
+ encoding: ASCII-8BIT
+ string: !binary |-
+ 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
+ http_version:
+ recorded_at: Fri, 17 May 2019 10:05:12 GMT
+recorded_with: VCR 4.0.0
diff --git a/spec/fixtures/files/bill_signature/serialized.json b/spec/fixtures/files/bill_signature/serialized.json
new file mode 100644
index 000000000..87852f06f
--- /dev/null
+++ b/spec/fixtures/files/bill_signature/serialized.json
@@ -0,0 +1 @@
+{"dossier1": "hash1", "dossier2": "hash2"}
diff --git a/spec/fixtures/files/bill_signature/signature.der b/spec/fixtures/files/bill_signature/signature.der
new file mode 100644
index 000000000..9adcc9a7e
Binary files /dev/null and b/spec/fixtures/files/bill_signature/signature.der differ
diff --git a/spec/lib/asn1/timestamp_spec.rb b/spec/lib/asn1/timestamp_spec.rb
new file mode 100644
index 000000000..3ae0d6272
--- /dev/null
+++ b/spec/lib/asn1/timestamp_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe ASN1::Timestamp do
+ let(:asn1timestamp) { File.read('spec/fixtures/files/bill_signature/signature.der') }
+
+ describe '.timestamp_time' do
+ subject { described_class.signature_time(asn1timestamp) }
+
+ it { is_expected.to eq Time.zone.parse('2019-04-30 15:30:20 UTC') }
+ end
+
+ describe '.timestamp_signed_data' do
+ subject { described_class.signed_digest(asn1timestamp) }
+
+ let(:data) { Digest::SHA256.hexdigest('CECI EST UN BLOB') }
+
+ it { is_expected.to eq data }
+ end
+end
diff --git a/spec/lib/universign/api_spec.rb b/spec/lib/universign/api_spec.rb
new file mode 100644
index 000000000..124076ba5
--- /dev/null
+++ b/spec/lib/universign/api_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe Universign::API do
+ describe '.request_timestamp', vcr: { cassette_name: 'universign' } do
+ subject { described_class.timestamp(digest) }
+
+ let(:digest) { Digest::SHA256.hexdigest("CECI EST UN HASH") }
+
+ it { is_expected.not_to be_nil }
+ end
+end
diff --git a/spec/models/bill_signature_spec.rb b/spec/models/bill_signature_spec.rb
new file mode 100644
index 000000000..30157cbfe
--- /dev/null
+++ b/spec/models/bill_signature_spec.rb
@@ -0,0 +1,160 @@
+require 'rails_helper'
+
+RSpec.describe BillSignature, type: :model do
+ describe 'validations' do
+ subject(:bill_signature) { BillSignature.new }
+
+ describe 'check_bill_digest' do
+ before do
+ bill_signature.dossier_operation_logs = dossier_operation_logs
+ bill_signature.digest = digest
+ bill_signature.valid?
+ end
+
+ context 'when there are no operations' do
+ let(:dossier_operation_logs) { [] }
+
+ context 'when the digest is correct' do
+ let(:digest) { Digest::SHA256.hexdigest('{}') }
+
+ it { expect(bill_signature.errors.details[:digest]).to be_empty }
+ end
+
+ context 'when the digest is incorrect' do
+ let(:digest) { 'baadf00d' }
+
+ it { expect(bill_signature.errors.details[:digest]).to eq [error: :invalid] }
+ end
+ end
+
+ context 'when the signature has operations' do
+ let(:dossier_operation_logs) { [build(:dossier_operation_log, id: '1234', digest: 'abcd')] }
+
+ context 'when the digest is correct' do
+ let(:digest) { Digest::SHA256.hexdigest('{"1234":"abcd"}') }
+
+ it { expect(bill_signature.errors.details[:digest]).to be_empty }
+ end
+
+ context 'when the digest is incorrect' do
+ let(:digest) { 'baadf00d' }
+
+ it { expect(bill_signature.errors.details[:digest]).to eq [error: :invalid] }
+ end
+ end
+ end
+
+ describe 'check_serialized_bill_contents' do
+ before do
+ bill_signature.dossier_operation_logs = dossier_operation_logs
+ bill_signature.serialized.attach(io: StringIO.new(serialized), filename: 'file') if serialized.present?
+ bill_signature.valid?
+ end
+
+ context 'when there are no operations' do
+ let(:dossier_operation_logs) { [] }
+ let(:serialized) { '{}' }
+
+ it { expect(bill_signature.errors.details[:serialized]).to be_empty }
+ end
+
+ context 'when the signature has operations' do
+ let(:dossier_operation_logs) { [build(:dossier_operation_log, id: '1234', digest: 'abcd')] }
+ let(:serialized) { '{"1234":"abcd"}' }
+
+ it { expect(bill_signature.errors.details[:serialized]).to be_empty }
+ end
+
+ context 'when serialized isn’t set' do
+ let(:dossier_operation_logs) { [] }
+ let(:serialized) { nil }
+
+ it { expect(bill_signature.errors.details[:serialized]).to eq [error: :blank] }
+ end
+ end
+
+ describe 'check_signature_contents' do
+ before do
+ bill_signature.signature.attach(io: StringIO.new(signature), filename: 'file') if signature.present?
+ allow(ASN1::Timestamp).to receive(:signature_time).and_return(signature_time)
+ allow(ASN1::Timestamp).to receive(:signed_digest).and_return(signed_digest)
+ bill_signature.digest = digest
+ bill_signature.valid?
+ end
+
+ context 'when the signature is correct' do
+ let(:signature) { 'signature' }
+ let(:signature_time) { 1.day.ago }
+ let(:digest) { 'abcd' }
+ let(:signed_digest) { 'abcd' }
+
+ it { expect(bill_signature.errors.details[:signature]).to be_empty }
+ end
+
+ context 'when the signature isn’t set' do
+ let(:signature) { nil }
+ let(:signature_time) { 1.day.ago }
+ let(:digest) { 'abcd' }
+ let(:signed_digest) { 'abcd' }
+
+ it { expect(bill_signature.errors.details[:signature]).to eq [error: :blank] }
+ end
+
+ context 'when the signature time is in the future' do
+ let(:signature) { 'signature' }
+ let(:signature_time) { 1.day.from_now }
+ let(:digest) { 'abcd' }
+ let(:signed_digest) { 'abcd' }
+
+ it { expect(bill_signature.errors.details[:signature]).to eq [error: :invalid_date] }
+ end
+
+ context 'when the signature doesn’t match the digest' do
+ let(:signature) { 'signature' }
+ let(:signature_time) { 1.day.ago }
+ let(:digest) { 'abcd' }
+ let(:signed_digest) { 'dcba' }
+
+ it { expect(bill_signature.errors.details[:signature]).to eq [error: :invalid] }
+ end
+ end
+ end
+
+ describe '.build_with_operations' do
+ subject(:bill_signature) { described_class.build_with_operations(dossier_operation_logs, Date.new(1871, 03, 18)) }
+
+ context 'when there are no operations' do
+ let(:dossier_operation_logs) { [] }
+
+ it { expect(bill_signature.operations_bill).to eq({}) }
+ it { expect(bill_signature.digest).to eq(Digest::SHA256.hexdigest('{}')) }
+ it { expect(bill_signature.serialized.download).to eq('{}') }
+ it { expect(bill_signature.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') }
+ end
+
+ context 'when there is one operation' do
+ let(:dossier_operation_logs) do
+ [build(:dossier_operation_log, id: '1234', digest: 'abcd')]
+ end
+
+ it { expect(bill_signature.operations_bill).to eq({ '1234' => 'abcd' }) }
+ it { expect(bill_signature.digest).to eq(Digest::SHA256.hexdigest('{"1234":"abcd"}')) }
+ it { expect(bill_signature.serialized.download).to eq('{"1234":"abcd"}') }
+ it { expect(bill_signature.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') }
+ end
+
+ context 'when there are several operations' do
+ let(:dossier_operation_logs) do
+ [
+ build(:dossier_operation_log, id: '1234', digest: 'abcd'),
+ build(:dossier_operation_log, id: '5678', digest: 'dcba')
+ ]
+ end
+
+ it { expect(bill_signature.operations_bill).to eq({ '1234' => 'abcd', '5678' => 'dcba' }) }
+ it { expect(bill_signature.digest).to eq(Digest::SHA256.hexdigest('{"1234":"abcd","5678":"dcba"}')) }
+ it { expect(bill_signature.serialized.download).to eq('{"1234":"abcd","5678":"dcba"}') }
+ it { expect(bill_signature.serialized.filename).to eq('demarches-simplifiees-operations-1871-03-18.json') }
+ end
+ end
+end
diff --git a/spec/services/bill_signature_service_spec.rb b/spec/services/bill_signature_service_spec.rb
new file mode 100644
index 000000000..2a88478df
--- /dev/null
+++ b/spec/services/bill_signature_service_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe BillSignatureService do
+ describe ".grouped_unsigned_operation_until" do
+ subject { BillSignatureService.grouped_unsigned_operation_until(date).length }
+
+ let(:date) { Date.today }
+
+ context "when operations of several days need to be signed" do
+ before do
+ create :dossier_operation_log, executed_at: 3.days.ago
+ create :dossier_operation_log, executed_at: 2.days.ago
+ create :dossier_operation_log, executed_at: 1.day.ago
+ end
+
+ it { is_expected.to eq 3 }
+ end
+
+ context "when operations on a single day need to be signed" do
+ before do
+ create :dossier_operation_log, executed_at: 1.day.ago
+ create :dossier_operation_log, executed_at: 1.day.ago
+ end
+
+ it { is_expected.to eq 1 }
+ end
+
+ context "when there are no operations to be signed" do
+ before do
+ create :dossier_operation_log, created_at: 1.day.ago, bill_signature: build(:bill_signature)
+ create :dossier_operation_log, created_at: 1.day.from_now
+ end
+
+ it { is_expected.to eq 0 }
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index cc2d47945..3417a9550 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3533,9 +3533,9 @@ fs.realpath@^1.0.0:
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
fsevents@^1.2.7:
- version "1.2.8"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.8.tgz#57ea5320f762cd4696e5e8e87120eccc8b11cacf"
- integrity sha512-tPvHgPGB7m40CZ68xqFGkKuzN+RnpGmSV+hgeKxhRpbxdqKXUFJGC3yonBOLzQBcJyGpdZFDfCsdOC2KFsXzeA==
+ version "1.2.9"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f"
+ integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==
dependencies:
nan "^2.12.1"
node-pre-gyp "^0.12.0"
@@ -4788,16 +4788,6 @@ lodash._reinterpolate@~3.0.0:
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
-lodash.assign@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
- integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=
-
-lodash.clonedeep@^4.3.2:
- version "4.5.0"
- resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
- integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
-
lodash.get@^4.0, lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
@@ -4813,11 +4803,6 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
-lodash.mergewith@^4.6.0:
- version "4.6.1"
- resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
- integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==
-
lodash.tail@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
@@ -5197,11 +5182,16 @@ mute-stream@0.0.7:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
-nan@^2.10.0, nan@^2.12.1:
+nan@^2.12.1:
version "2.13.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
+nan@^2.13.2:
+ version "2.14.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
+ integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
+
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -5324,9 +5314,9 @@ node-releases@^1.1.14:
semver "^5.3.0"
node-sass@^4.11.0:
- version "4.11.0"
- resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a"
- integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==
+ version "4.12.0"
+ resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.12.0.tgz#0914f531932380114a30cc5fa4fa63233a25f017"
+ integrity sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==
dependencies:
async-foreach "^0.1.3"
chalk "^1.1.1"
@@ -5335,12 +5325,10 @@ node-sass@^4.11.0:
get-stdin "^4.0.1"
glob "^7.0.3"
in-publish "^2.0.0"
- lodash.assign "^4.2.0"
- lodash.clonedeep "^4.3.2"
- lodash.mergewith "^4.6.0"
+ lodash "^4.17.11"
meow "^3.7.0"
mkdirp "^0.5.1"
- nan "^2.10.0"
+ nan "^2.13.2"
node-gyp "^3.8.0"
npmlog "^4.0.0"
request "^2.88.0"