Env-driven seed for admin user, API key, and webhook URL

Adds `config/initializers/env_seed.rb` — idempotent, resilient
boot-time seeder that replaces both the manual /setup wizard run and
our standalone `bin/seed-docuseal-webhook` script. Works in dev and
prod: just set the env vars in the deploy config.

When `DOCUSEAL_ADMIN_EMAIL` + `DOCUSEAL_ADMIN_PASSWORD` are set, on
every boot the initializer upserts:

  * Account (org row, name/timezone/locale from env)
  * User (admin, password updated only if encrypted_password blank)
  * AccessToken (rewritten when `DOCUSEAL_API_KEY` set and differs)
  * EncryptedConfig APP_URL_KEY + ESIGN_CERTS_KEY
  * WebhookUrl (when `DOCUSEAL_WEBHOOK_URL` set; events optional)

Resilience:
  * Skips quietly when admin email/password missing — `/setup` still works
  * Catches NoDatabaseError / ConnectionNotEstablished / StatementInvalid
    so `db:create` before `db:migrate` doesn't crash boot
  * Re-runs are no-ops (find_or_initialize_by + diff before save)
  * Survives DB drop + recreate: rows are re-seeded on next boot

.env.example documents the new vars.
pull/697/head
Vadym Shaveiko 3 months ago
parent f4324122cc
commit e59c8b421e

@ -20,3 +20,18 @@ EMBED_ALLOWED_ORIGIN=https://localhost:4500
# Override host base URL used in generated links if needed.
# HOST=https://localhost:3030
# ── Boot-time seeding (config/initializers/env_seed.rb) ─────────────────
# When EMAIL + PASSWORD are both set, the admin account + API key +
# webhook target are upserted on every boot. Skips entirely if either
# is missing — leave them unset to provision through /setup by hand.
DOCUSEAL_ADMIN_EMAIL=admin@enwella.local
DOCUSEAL_ADMIN_PASSWORD=password123!
# Optional: pin the API key (otherwise an auto-generated token is kept).
# DOCUSEAL_API_KEY=
# Optional: register a webhook callback for the host EHR. Comma-list of
# events (defaults to form.completed,template.created).
# DOCUSEAL_WEBHOOK_URL=https://localhost:3000/webhooks/docuseal?token=
# DOCUSEAL_WEBHOOK_EVENTS=form.completed,template.created
# DOCUSEAL_ACCOUNT_NAME=DocuSeal
# DOCUSEAL_APP_URL=https://localhost:3030

@ -0,0 +1,98 @@
# frozen_string_literal: true
# Boot-time seeder for the self-hosted / embedded deployment. Reads
# DOCUSEAL_* env vars and idempotently upserts the rows that the upstream
# /setup wizard would otherwise create through the UI:
#
# * Account (organisation row)
# * User (admin login)
# * AccessToken (API key)
# * EncryptedConfig (APP_URL — required by url helpers)
# * WebhookUrl (callback target subscribed to a fixed event set)
#
# Works in development and production. Safe to run on every boot:
#
# * Skips entirely when DOCUSEAL_ADMIN_EMAIL / DOCUSEAL_ADMIN_PASSWORD
# are absent (lets the human /setup flow run unmolested if you'd
# rather provision by hand).
# * Resilient to a fresh DB (`db:create` before `db:migrate`): catches
# `ActiveRecord::NoDatabaseError` and `ActiveRecord::StatementInvalid`
# so the boot doesn't crash before migrations have a chance to run.
# * Each upsert is keyed on a natural identifier (email / sha1(url))
# so re-runs do not duplicate.
# * `AccessToken#token` is only rewritten when DOCUSEAL_API_KEY is set
# AND differs from what's already stored — otherwise an existing
# auto-generated token stays put.
Rails.application.config.after_initialize do
email = ENV['DOCUSEAL_ADMIN_EMAIL'].to_s.strip
password = ENV['DOCUSEAL_ADMIN_PASSWORD'].to_s
next if email.empty? || password.empty?
begin
next unless ActiveRecord::Base.connection.data_source_exists?('users')
rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished,
ActiveRecord::StatementInvalid, PG::Error
next
end
begin
ActiveRecord::Base.transaction do
account = Account.first || Account.create!(
name: ENV.fetch('DOCUSEAL_ACCOUNT_NAME', 'DocuSeal'),
timezone: ENV.fetch('DOCUSEAL_TIMEZONE', 'UTC'),
locale: ENV.fetch('DOCUSEAL_LOCALE', 'en-US')
)
user = User.find_or_initialize_by(email: email)
user.account = account
user.role = User::ADMIN_ROLE
user.first_name = ENV.fetch('DOCUSEAL_ADMIN_FIRST_NAME', 'Admin')
user.last_name = ENV.fetch('DOCUSEAL_ADMIN_LAST_NAME', '')
user.password = password if user.new_record? || user.encrypted_password.blank?
user.skip_confirmation! if user.respond_to?(:skip_confirmation!) && !user.confirmed?
user.save!
desired_token = ENV['DOCUSEAL_API_KEY'].to_s.strip
if desired_token.present?
token = user.access_tokens.first_or_initialize
if token.new_record? || token.token != desired_token
token.token = desired_token
token.save!
end
end
app_url = ENV.fetch('DOCUSEAL_APP_URL', "http://localhost:#{ENV.fetch('PORT', 3000)}")
app_url_config = account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::APP_URL_KEY)
if app_url_config.new_record? || app_url_config.value != app_url
app_url_config.value = app_url
app_url_config.save!
end
if account.encrypted_configs.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).blank?
account.encrypted_configs.create!(
key: EncryptedConfig::ESIGN_CERTS_KEY,
value: GenerateCertificate.call.transform_values(&:to_pem)
)
end
if SearchEntry.table_exists? &&
account.account_configs.find_by(key: 'fulltext_search').blank?
account.account_configs.create!(key: :fulltext_search, value: true)
end
webhook_url = ENV['DOCUSEAL_WEBHOOK_URL'].to_s.strip
if webhook_url.present?
events = ENV.fetch('DOCUSEAL_WEBHOOK_EVENTS', 'form.completed,template.created')
.split(',').map(&:strip).reject(&:empty?)
row = account.webhook_urls.find_or_initialize_by(sha1: Digest::SHA1.hexdigest(webhook_url))
row.url = webhook_url
row.events = events
row.save!
end
Docuseal.refresh_default_url_options! if defined?(Docuseal) && Docuseal.respond_to?(:refresh_default_url_options!)
end
rescue StandardError => e
Rails.logger.warn("[env_seed] skipped: #{e.class}: #{e.message}")
end
end
Loading…
Cancel
Save