From d26df78c700d5123606e1e46e87e97871f5b9161 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 17 Aug 2026 23:41:59 -0400 Subject: [PATCH] add admin creation to deployment --- scripts/create-admin-user.cjs | 35 ++++++++++------- scripts/docker-prod-common.sh | 73 +++++++++++++++++++++++++++++++++++ scripts/docker-prod-deploy.sh | 3 ++ 3 files changed, 98 insertions(+), 13 deletions(-) diff --git a/scripts/create-admin-user.cjs b/scripts/create-admin-user.cjs index 4644376..48f823a 100644 --- a/scripts/create-admin-user.cjs +++ b/scripts/create-admin-user.cjs @@ -40,24 +40,33 @@ async function main() { normalizeDatabaseUrlForExecution() const email = requireValue('email', 'ADMIN_EMAIL').toLowerCase() - const password = requireValue('password', 'ADMIN_PASSWORD') - const firstName = readValue('first-name', 'ADMIN_FIRST_NAME', 'Admin').trim() - const lastName = readValue('last-name', 'ADMIN_LAST_NAME', 'User').trim() - const role = readValue('role', 'ADMIN_ROLE', 'SUPER_ADMIN').trim().toUpperCase() - const updateExisting = readBoolean('update-existing', 'ADMIN_UPDATE_EXISTING', false) - - if (!ADMIN_ROLES.has(role)) { - throw new Error(`Invalid ADMIN_ROLE "${role}". Use one of: ${Array.from(ADMIN_ROLES).join(', ')}`) - } - - if (password.length < 8) { - throw new Error('Admin password must be at least 8 characters long.') - } + const checkOnly = process.argv.includes('--check') const { PrismaClient } = require('../packages/database/generated') const prisma = new PrismaClient() try { const existing = await prisma.adminUser.findUnique({ where: { email } }) + + if (checkOnly) { + console.log(existing ? 'EXISTS' : 'MISSING') + process.exitCode = existing ? 0 : 1 + return + } + + const password = requireValue('password', 'ADMIN_PASSWORD') + const firstName = readValue('first-name', 'ADMIN_FIRST_NAME', 'Admin').trim() + const lastName = readValue('last-name', 'ADMIN_LAST_NAME', 'User').trim() + const role = readValue('role', 'ADMIN_ROLE', 'SUPER_ADMIN').trim().toUpperCase() + const updateExisting = readBoolean('update-existing', 'ADMIN_UPDATE_EXISTING', false) + + if (!ADMIN_ROLES.has(role)) { + throw new Error(`Invalid ADMIN_ROLE "${role}". Use one of: ${Array.from(ADMIN_ROLES).join(', ')}`) + } + + if (password.length < 8) { + throw new Error('Admin password must be at least 8 characters long.') + } + const passwordHash = await bcrypt.hash(password, 12) if (existing) { diff --git a/scripts/docker-prod-common.sh b/scripts/docker-prod-common.sh index a395a39..8242cc7 100644 --- a/scripts/docker-prod-common.sh +++ b/scripts/docker-prod-common.sh @@ -406,6 +406,79 @@ run_prod_migrations() { prod_compose run --rm migrate } +# Ensures a platform admin account exists for the production database, prompting +# interactively only when stdin is a TTY. Runs inside the API container so it can +# reach Postgres on the internal Docker network (production Postgres is not exposed +# to the host). +# +# Behavior: +# - Admin already exists -> ask whether to update the password (y/N). If yes, +# prompt for a new password and update it. If no, keep +# the existing password and continue deployment. +# - Admin does not exist -> create it and prompt for a password. +ensure_prod_admin() { + export_env_value_if_unset ADMIN_SEED_EMAIL + export_env_value_if_unset ADMIN_SEED_FIRST_NAME + export_env_value_if_unset ADMIN_SEED_LAST_NAME + + local email="${ADMIN_SEED_EMAIL:-rentaldrivego@gmail.com}" + local first_name="${ADMIN_SEED_FIRST_NAME:-Super}" + local last_name="${ADMIN_SEED_LAST_NAME:-Admin}" + + if [[ ! -t 0 ]]; then + echo "Skipping interactive admin setup (stdin is not a TTY)." + return 0 + fi + + local admin_exists=0 + echo "Checking admin user ${email}..." + if prod_compose run --rm \ + -v "${ROOT_DIR}/scripts:/app/scripts:ro" \ + -e ADMIN_EMAIL="${email}" \ + api node scripts/create-admin-user.cjs --check; then + admin_exists=1 + fi + + if [[ ${admin_exists} -eq 1 ]]; then + echo "Admin user ${email} already exists." + local update_answer + read -r -p "Update its password? [y/N] " update_answer + case "${update_answer}" in + y|Y|yes|YES|Yes) ;; + *) + echo "Keeping existing admin password." + return 0 + ;; + esac + else + echo "Admin user ${email} does not exist; it will be created." + fi + + local password="" + while [[ ${#password} -lt 8 ]]; do + if ! read -r -s -p "Enter admin password (min 8 characters): " password; then + echo + echo "No password provided; aborting admin setup." >&2 + return 1 + fi + echo + if [[ ${#password} -lt 8 ]]; then + echo "Password must be at least 8 characters." >&2 + fi + done + + prod_compose run --rm \ + -v "${ROOT_DIR}/scripts:/app/scripts:ro" \ + -e ADMIN_EMAIL="${email}" \ + -e ADMIN_PASSWORD="${password}" \ + -e ADMIN_FIRST_NAME="${first_name}" \ + -e ADMIN_LAST_NAME="${last_name}" \ + -e ADMIN_UPDATE_EXISTING=true \ + api node scripts/create-admin-user.cjs + + echo "Admin user ${email} is ready." +} + wait_for_healthy() { local service="$1" local timeout="${2:-120}" diff --git a/scripts/docker-prod-deploy.sh b/scripts/docker-prod-deploy.sh index d4bf67e..01f48d0 100644 --- a/scripts/docker-prod-deploy.sh +++ b/scripts/docker-prod-deploy.sh @@ -25,6 +25,9 @@ wait_for_healthy postgres 120 echo "Running database migrations" run_prod_migrations +echo "Ensuring platform admin account" +ensure_prod_admin + echo "Starting application services" prod_compose up -d api homepage carplace dashboard admin wait_for_healthy api 180