From ec0a1ddfd0807faadba28cf18332ce1135076016 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sat, 13 Dec 2025 09:12:27 -0700 Subject: [PATCH 1/8] added common config check --- src/commands/common-config-check.ts | 354 ++++++++++++++++++++++++++++ src/commands/index.ts | 1 + 2 files changed, 355 insertions(+) create mode 100644 src/commands/common-config-check.ts diff --git a/src/commands/common-config-check.ts b/src/commands/common-config-check.ts new file mode 100644 index 0000000..57fc1d4 --- /dev/null +++ b/src/commands/common-config-check.ts @@ -0,0 +1,354 @@ +import {Command, Option} from "@commander-js/extra-typings"; +import {FusionAuthClient} from '@fusionauth/typescript-client'; +import chalk from "chalk"; +import {errorAndExit} from '../utils.js'; +import {apiKeyOption, hostOption} from "../options.js"; + +interface CheckResult { + passed: boolean; + message: string; +} + +const skipLicenseCheckOption = new Option( + '--skip-license-check', + 'Skip checking license activation (for community edition users)' +).default(false); + +const action = async function ({key: apiKey, host, skipLicenseCheck}: { + key: string; + host: string; + skipLicenseCheck: boolean; +}) { + console.log(chalk.blue(`Checking common configuration on ${host}...`)); + + const results: CheckResult[] = []; + let allPassed = true; + + try { + const fusionAuthClient = new FusionAuthClient(apiKey, host); + + // Check 1: API Key permissions by creating and deleting a test admin user + console.log(chalk.cyan('\n1. Checking API key permissions...')); + try { + const testEmail = `test-admin-${Date.now()}@fusionauth-cli-check.local`; + const testPassword = `TestPass-${Date.now()}-!@#$`; + + // Create test user + const createUserResponse = await fusionAuthClient.createUser(null!, { + user: { + email: testEmail, + password: testPassword, + username: `test_admin_${Date.now()}` + } + }); + + if (!createUserResponse.wasSuccessful()) { + results.push({ + passed: false, + message: 'API key lacks permissions to create users' + }); + allPassed = false; + } else if (!createUserResponse.response.user?.id) { + results.push({ + passed: false, + message: 'Created user but no user ID returned' + }); + allPassed = false; + } else { + const userId = createUserResponse.response.user.id; + + // Search for the FusionAuth application + const searchAppsResponse = await fusionAuthClient.searchApplications({ + search: { + name: 'FusionAuth' + } + }); + + if (!searchAppsResponse.wasSuccessful() || !searchAppsResponse.response.applications || searchAppsResponse.response.applications.length === 0) { + results.push({ + passed: false, + message: 'Could not find FusionAuth application' + }); + allPassed = false; + // Clean up + await fusionAuthClient.deleteUser(userId); + } else { + const fusionAuthApp = searchAppsResponse.response.applications[0]; + + if (!fusionAuthApp.id) { + results.push({ + passed: false, + message: 'FusionAuth application has no ID' + }); + allPassed = false; + // Clean up + await fusionAuthClient.deleteUser(userId); + } else { + // Register user to FusionAuth app with admin role + const registrationResponse = await fusionAuthClient.register(userId, { + registration: { + applicationId: fusionAuthApp.id, + roles: ['admin'] + } + }); + + // Clean up - delete test user + await fusionAuthClient.deleteUser(userId); + + if (!registrationResponse.wasSuccessful()) { + results.push({ + passed: false, + message: 'API key lacks permissions to register users with admin role' + }); + allPassed = false; + } else { + results.push({ + passed: true, + message: 'API key has appropriate permissions ✓' + }); + } + } + } + } + } catch (e) { + results.push({ + passed: false, + message: `API key permission check failed: ${e instanceof Error ? e.message : String(e)}` + }); + allPassed = false; + } + + // Check 2: Email server configuration + console.log(chalk.cyan('\n2. Checking email server configuration...')); + try { + const tenantResponse = await fusionAuthClient.retrieveTenants(); + if (!tenantResponse.wasSuccessful()) { + results.push({ + passed: false, + message: 'Could not retrieve tenants' + }); + allPassed = false; + } else { + const tenants = tenantResponse.response.tenants || []; + let emailConfigured = false; + + for (const tenant of tenants) { + if (tenant.emailConfiguration?.host) { + emailConfigured = true; + break; + } + } + + if (emailConfigured) { + results.push({ + passed: true, + message: 'Email server is configured ✓' + }); + } else { + results.push({ + passed: false, + message: 'Email server not configured - required for password resets' + }); + allPassed = false; + } + } + } catch (e) { + results.push({ + passed: false, + message: `Email configuration check failed: ${e instanceof Error ? e.message : String(e)}` + }); + allPassed = false; + } + + // Check 3: Multiple admin users + console.log(chalk.cyan('\n3. Checking for multiple admin users...')); + try { + // Search for the FusionAuth application + const searchAppsResponse = await fusionAuthClient.searchApplications({ + search: { + name: 'FusionAuth' + } + }); + + if (!searchAppsResponse.wasSuccessful() || !searchAppsResponse.response.applications || searchAppsResponse.response.applications.length === 0) { + results.push({ + passed: false, + message: 'Could not find FusionAuth application' + }); + allPassed = false; + } else { + const fusionAuthApp = searchAppsResponse.response.applications[0]; + + if (!fusionAuthApp.id) { + results.push({ + passed: false, + message: 'FusionAuth application has no ID' + }); + allPassed = false; + } else { + const searchResponse = await fusionAuthClient.searchUsersByQuery({ + search: { + queryString: `registrations.applicationId:${fusionAuthApp.id} AND registrations.roles:admin` + } + }); + + if (!searchResponse.wasSuccessful()) { + results.push({ + passed: false, + message: 'Could not search for admin users' + }); + allPassed = false; + } else { + const adminCount = searchResponse.response.total || 0; + + if (adminCount >= 2) { + results.push({ + passed: true, + message: `Found ${adminCount} admin users ✓` + }); + } else { + results.push({ + passed: false, + message: `Only ${adminCount} admin user(s) found - recommend at least 2 for redundancy` + }); + allPassed = false; + } + } + } + } + } catch (e) { + results.push({ + passed: false, + message: `Admin user check failed: ${e instanceof Error ? e.message : String(e)}` + }); + allPassed = false; + } + + // Check 4: License key activation (optional) + if (!skipLicenseCheck) { + console.log(chalk.cyan('\n4. Checking license key activation...')); + try { + const statusResponse = await fusionAuthClient.retrieveReactorStatus(); + + if (!statusResponse.wasSuccessful()) { + results.push({ + passed: false, + message: 'Could not retrieve Reactor status' + }); + allPassed = false; + } else { + const status = statusResponse.response.status; + + if (status?.licensed) { + results.push({ + passed: true, + message: 'License key is activated ✓' + }); + } else { + results.push({ + passed: false, + message: 'License key not activated - required for paid features' + }); + allPassed = false; + } + } + } catch (e) { + results.push({ + passed: false, + message: `License check failed: ${e instanceof Error ? e.message : String(e)}` + }); + allPassed = false; + } + } else { + console.log(chalk.cyan('\n4. Skipping license key check (--skip-license-check)...')); + } + + // Check 5: Tenant issuer configuration + console.log(chalk.cyan('\n5. Checking tenant issuer configuration...')); + try { + const tenantResponse = await fusionAuthClient.retrieveTenants(); + + if (!tenantResponse.wasSuccessful()) { + results.push({ + passed: false, + message: 'Could not retrieve tenants' + }); + allPassed = false; + } else { + const tenants = tenantResponse.response.tenants || []; + let hasDefaultIssuer = false; + let allIssuersConfigured = true; + + for (const tenant of tenants) { + const issuer = tenant.issuer || ''; + + if (issuer === 'acme.com') { + hasDefaultIssuer = true; + } + if (!issuer || issuer === 'acme.com') { + allIssuersConfigured = false; + } + } + + if (allIssuersConfigured && !hasDefaultIssuer) { + results.push({ + passed: true, + message: 'Tenant issuer(s) properly configured ✓' + }); + } else if (hasDefaultIssuer) { + results.push({ + passed: false, + message: 'Tenant issuer still set to default "acme.com" - should be changed to your domain' + }); + allPassed = false; + } else { + results.push({ + passed: false, + message: 'Some tenant issuer(s) not configured properly' + }); + allPassed = false; + } + } + } catch (e) { + results.push({ + passed: false, + message: `Tenant issuer check failed: ${e instanceof Error ? e.message : String(e)}` + }); + allPassed = false; + } + + // Print results + console.log(chalk.blue('\n\n=== Configuration Check Results ===\n')); + results.forEach((result, index) => { + if (result.passed) { + console.log(chalk.green(`✓ ${result.message}`)); + } else { + console.log(chalk.red(`✗ ${result.message}`)); + } + }); + + console.log(chalk.blue('\n===================================\n')); + + if (allPassed) { + console.log(chalk.green.bold('SUCCESS: All common configuration checks passed!')); + console.log(chalk.green('Your FusionAuth instance is properly configured.')); + } else { + console.log(chalk.yellow.bold('WARNING: Some configuration checks failed.')); + console.log(chalk.yellow('Please address the issues above to ensure proper FusionAuth operation.')); + console.log(chalk.yellow('\nFor more information, visit:')); + console.log(chalk.cyan('https://fusionauth.io/docs/get-started/download-and-install/common-configuration')); + process.exit(1); + } + + } catch (e: unknown) { + errorAndExit('Common configuration check error:', e); + } +} + +// noinspection JSUnusedGlobalSymbols +export const commonConfigCheck = new Command('commonConfigCheck') + .description('Checks for common configuration settings that should be changed') + .addOption(apiKeyOption) + .addOption(hostOption) + .addOption(skipLicenseCheckOption) + .action(action); diff --git a/src/commands/index.ts b/src/commands/index.ts index d1dd673..0b841f7 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,3 +1,4 @@ +export * from './common-config-check.js'; export * from './email-create.js'; export * from './email-download.js'; export * from './email-duplicate.js'; From b24c1ad9c7ffc24b3c616ecd8add68a771bdcfbb Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sat, 13 Dec 2025 09:14:30 -0700 Subject: [PATCH 2/8] added common config check to doc --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6d94ff6..19d076e 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ fusionauth --help; ``` Currently, the CLI supports the following commands: +- Common config check + - `fusionauth common-config-check` - Checks to make sure common configuration settings are set. - Emails - `fusionauth email:download` - Download a specific template or all email templates from a FusionAuth server. - `fusionauth email:duplicate` - Duplicate an email template locally. From a7c6008228195bc88ecb49dd14f9d1a5feb4b897 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sat, 13 Dec 2025 09:15:04 -0700 Subject: [PATCH 3/8] changing name to fit convention --- src/commands/common-config-check.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/common-config-check.ts b/src/commands/common-config-check.ts index 57fc1d4..6fafa22 100644 --- a/src/commands/common-config-check.ts +++ b/src/commands/common-config-check.ts @@ -346,7 +346,7 @@ const action = async function ({key: apiKey, host, skipLicenseCheck}: { } // noinspection JSUnusedGlobalSymbols -export const commonConfigCheck = new Command('commonConfigCheck') +export const commonConfigCheck = new Command('common-config-check') .description('Checks for common configuration settings that should be changed') .addOption(apiKeyOption) .addOption(hostOption) From 0191d1be5c122544a2cb959284eaea1363f1dcef Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sat, 13 Dec 2025 12:01:35 -0700 Subject: [PATCH 4/8] trying to fix admin user adding/removing causing issues error --- src/commands/common-config-check.ts | 142 ++++++++++++++-------------- 1 file changed, 72 insertions(+), 70 deletions(-) diff --git a/src/commands/common-config-check.ts b/src/commands/common-config-check.ts index 6fafa22..55979c3 100644 --- a/src/commands/common-config-check.ts +++ b/src/commands/common-config-check.ts @@ -27,8 +27,73 @@ const action = async function ({key: apiKey, host, skipLicenseCheck}: { try { const fusionAuthClient = new FusionAuthClient(apiKey, host); - // Check 1: API Key permissions by creating and deleting a test admin user - console.log(chalk.cyan('\n1. Checking API key permissions...')); + // Check 1: Multiple admin users + console.log(chalk.cyan('\n1. Checking for multiple admin users...')); + try { + // Search for the FusionAuth application + const searchAppsResponse = await fusionAuthClient.searchApplications({ + search: { + name: 'FusionAuth' + } + }); + + if (!searchAppsResponse.wasSuccessful() || !searchAppsResponse.response.applications || searchAppsResponse.response.applications.length === 0) { + results.push({ + passed: false, + message: 'Could not find FusionAuth application' + }); + allPassed = false; + } else { + const fusionAuthApp = searchAppsResponse.response.applications[0]; + + if (!fusionAuthApp.id) { + results.push({ + passed: false, + message: 'FusionAuth application has no ID' + }); + allPassed = false; + } else { + const searchResponse = await fusionAuthClient.searchUsersByQuery({ + search: { + queryString: `registrations.applicationId:${fusionAuthApp.id} AND registrations.roles:admin`, + accurateTotal: true + } + }); + + if (!searchResponse.wasSuccessful()) { + results.push({ + passed: false, + message: 'Could not search for admin users' + }); + allPassed = false; + } else { + const adminCount = searchResponse.response.total || 0; + + if (adminCount >= 2) { + results.push({ + passed: true, + message: `Found ${adminCount} admin users ✓` + }); + } else { + results.push({ + passed: false, + message: `Only ${adminCount} admin user(s) found - recommend at least 2 for redundancy` + }); + allPassed = false; + } + } + } + } + } catch (e) { + results.push({ + passed: false, + message: `Admin user check failed: ${e instanceof Error ? e.message : String(e)}` + }); + allPassed = false; + } + + // Check 2: API Key permissions by creating and deleting a test admin user + console.log(chalk.cyan('\n2. Checking API key permissions...')); try { const testEmail = `test-admin-${Date.now()}@fusionauth-cli-check.local`; const testPassword = `TestPass-${Date.now()}-!@#$`; @@ -118,8 +183,8 @@ const action = async function ({key: apiKey, host, skipLicenseCheck}: { allPassed = false; } - // Check 2: Email server configuration - console.log(chalk.cyan('\n2. Checking email server configuration...')); + // Check 3: Email server configuration + console.log(chalk.cyan('\n3. Checking email server configuration...')); try { const tenantResponse = await fusionAuthClient.retrieveTenants(); if (!tenantResponse.wasSuccessful()) { @@ -133,7 +198,8 @@ const action = async function ({key: apiKey, host, skipLicenseCheck}: { let emailConfigured = false; for (const tenant of tenants) { - if (tenant.emailConfiguration?.host) { + const emailHost = tenant.emailConfiguration?.host; + if (emailHost && emailHost !== 'localhost') { emailConfigured = true; break; } @@ -147,7 +213,7 @@ const action = async function ({key: apiKey, host, skipLicenseCheck}: { } else { results.push({ passed: false, - message: 'Email server not configured - required for password resets' + message: 'Email server not configured or set to default "localhost" - required for password resets' }); allPassed = false; } @@ -160,70 +226,6 @@ const action = async function ({key: apiKey, host, skipLicenseCheck}: { allPassed = false; } - // Check 3: Multiple admin users - console.log(chalk.cyan('\n3. Checking for multiple admin users...')); - try { - // Search for the FusionAuth application - const searchAppsResponse = await fusionAuthClient.searchApplications({ - search: { - name: 'FusionAuth' - } - }); - - if (!searchAppsResponse.wasSuccessful() || !searchAppsResponse.response.applications || searchAppsResponse.response.applications.length === 0) { - results.push({ - passed: false, - message: 'Could not find FusionAuth application' - }); - allPassed = false; - } else { - const fusionAuthApp = searchAppsResponse.response.applications[0]; - - if (!fusionAuthApp.id) { - results.push({ - passed: false, - message: 'FusionAuth application has no ID' - }); - allPassed = false; - } else { - const searchResponse = await fusionAuthClient.searchUsersByQuery({ - search: { - queryString: `registrations.applicationId:${fusionAuthApp.id} AND registrations.roles:admin` - } - }); - - if (!searchResponse.wasSuccessful()) { - results.push({ - passed: false, - message: 'Could not search for admin users' - }); - allPassed = false; - } else { - const adminCount = searchResponse.response.total || 0; - - if (adminCount >= 2) { - results.push({ - passed: true, - message: `Found ${adminCount} admin users ✓` - }); - } else { - results.push({ - passed: false, - message: `Only ${adminCount} admin user(s) found - recommend at least 2 for redundancy` - }); - allPassed = false; - } - } - } - } - } catch (e) { - results.push({ - passed: false, - message: `Admin user check failed: ${e instanceof Error ? e.message : String(e)}` - }); - allPassed = false; - } - // Check 4: License key activation (optional) if (!skipLicenseCheck) { console.log(chalk.cyan('\n4. Checking license key activation...')); From 816a3cb282f9c943818ae398cfbc8448f2fb9dba Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sat, 13 Dec 2025 12:59:22 -0700 Subject: [PATCH 5/8] added troubleshooting section --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 19d076e..d80899d 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,10 @@ npx fusionauth -h; To see examples of use look at https://fusionauth.io/docs/v1/tech/lambdas/testing. +## Troubleshooting + +If you run this multiple times in a row against a local instance, the number of admin users may be incorrect until you re-index. See [this issue for more](https://github.com/FusionAuth/fusionauth-issues/issues/3271). + ## License This code is available as open source under the terms of the [Apache v2.0 License](https://opensource.org/licenses/Apache-2.0). From a296a93a0e7b4c22dd6b1645d0664dcdc7aa4d83 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Sat, 13 Dec 2025 13:01:49 -0700 Subject: [PATCH 6/8] ignore vim swp files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 8369d01..584b6cf 100644 --- a/.gitignore +++ b/.gitignore @@ -212,3 +212,6 @@ dist # FusionAuth data folders /tpl/ lambdas/ +emails/ + +*.swp From 2bfa95d5877e758ccf5d47ac87d3aaa6f6f03544 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Wed, 24 Dec 2025 17:04:31 -0700 Subject: [PATCH 7/8] renamed command to be more consistent, per PR feedback --- .../{common-config-check.ts => check-common-config.ts} | 4 ++-- src/commands/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/commands/{common-config-check.ts => check-common-config.ts} (99%) diff --git a/src/commands/common-config-check.ts b/src/commands/check-common-config.ts similarity index 99% rename from src/commands/common-config-check.ts rename to src/commands/check-common-config.ts index 55979c3..b35f23b 100644 --- a/src/commands/common-config-check.ts +++ b/src/commands/check-common-config.ts @@ -11,7 +11,7 @@ interface CheckResult { const skipLicenseCheckOption = new Option( '--skip-license-check', - 'Skip checking license activation (for community edition users)' + 'Skip checking license activation (for community plan users)' ).default(false); const action = async function ({key: apiKey, host, skipLicenseCheck}: { @@ -348,7 +348,7 @@ const action = async function ({key: apiKey, host, skipLicenseCheck}: { } // noinspection JSUnusedGlobalSymbols -export const commonConfigCheck = new Command('common-config-check') +export const checkCommonConfig = new Command('check:commonConfig') .description('Checks for common configuration settings that should be changed') .addOption(apiKeyOption) .addOption(hostOption) diff --git a/src/commands/index.ts b/src/commands/index.ts index 0b841f7..17b7efa 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,4 +1,4 @@ -export * from './common-config-check.js'; +export * from './check-common-config.js'; export * from './email-create.js'; export * from './email-download.js'; export * from './email-duplicate.js'; From 572d80cce50535a64ff49f51728e6f541a1ccea1 Mon Sep 17 00:00:00 2001 From: Dan Moore Date: Wed, 24 Dec 2025 21:50:15 -0700 Subject: [PATCH 8/8] corrected command name, added to readme --- README.md | 2 +- src/commands/check-common-config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d80899d..2160b75 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ fusionauth --help; Currently, the CLI supports the following commands: - Common config check - - `fusionauth common-config-check` - Checks to make sure common configuration settings are set. + - `fusionauth check:common-config` - Checks to make sure common configuration settings are set. - Emails - `fusionauth email:download` - Download a specific template or all email templates from a FusionAuth server. - `fusionauth email:duplicate` - Duplicate an email template locally. diff --git a/src/commands/check-common-config.ts b/src/commands/check-common-config.ts index b35f23b..8c051a0 100644 --- a/src/commands/check-common-config.ts +++ b/src/commands/check-common-config.ts @@ -348,7 +348,7 @@ const action = async function ({key: apiKey, host, skipLicenseCheck}: { } // noinspection JSUnusedGlobalSymbols -export const checkCommonConfig = new Command('check:commonConfig') +export const checkCommonConfig = new Command('check:common-config') .description('Checks for common configuration settings that should be changed') .addOption(apiKeyOption) .addOption(hostOption)