From e59c8b421e47ebf3ae8c36f2ca2be7e3cc9a05d0 Mon Sep 17 00:00:00 2001 From: Vadym Shaveiko Date: Sun, 17 May 2026 01:17:50 -0400 Subject: [PATCH] Env-driven seed for admin user, API key, and webhook URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .env.example | 15 +++++ config/initializers/env_seed.rb | 98 +++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 config/initializers/env_seed.rb diff --git a/.env.example b/.env.example index 49f9f3da..70fad748 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/config/initializers/env_seed.rb b/config/initializers/env_seed.rb new file mode 100644 index 00000000..5addbcaa --- /dev/null +++ b/config/initializers/env_seed.rb @@ -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