Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
push:
pull_request:
workflow_dispatch:
pull_request:
branches:
- main

jobs:
test:
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ fusionauth --help;
```

Currently, the CLI supports the following commands:
- Application Update
- `fusionauth application:update <application-id>` - Updates an application with provided data
- `--host` - Required. Provide a FusionAuth host URL or add it via an environment variable (`FUSIONAUTH_HOST`)
- `--key` - Required. Provide an API key with permissions for updating the given application or add via an environment variable( `FUSIONAUTH_API_KEY`)
- `-d, --data <path-to-json-file>` - Provide a data file containing all the properties you wish to update constructed like the body of an application update
- `-p, --prop <property.to.change=value>` - Update a single property in the application
- `--redirect-url <redirectUrl>` - Update the Authorized redirect URL for your applicatoin
- `--example` - Create an example file with editable properties to use in conjunction with the `--data` flag
- Common config check
- `fusionauth check:common-config` - Checks to make sure common configuration settings are set.
- Emails
Expand Down
1 change: 1 addition & 0 deletions src/commands/application-update/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./update.js";
125 changes: 125 additions & 0 deletions src/commands/application-update/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Command } from "@commander-js/extra-typings";
import { __dirname, logEvent } from '../../utils.js'
import {
ApplyOptions,
ExecutionMetrics,
StepResult,
StepStatus,
ErrorCategory,
} from '../../utilities/apply/types.js';
import { HTTPClient } from '../../utilities/apply/http-client.js';
import { apiKeyOption, hostOption } from '../../options.js';
import path from "node:path";
import { readFileSync, writeFileSync } from "node:fs";
import chalk from "chalk";
import { exampleApplicationBody } from "../../utils.js";

function getData(file: string) {
const fileLoc = path.resolve(file)

const contentBuffer = readFileSync(fileLoc).toString('utf-8')
const contents = JSON.parse(contentBuffer)

return contents

}

function setNestedProps(obj: any, path: string, value: any) {
/* Takes object and dynamically applies a property at any depth
myprop.somedepth.key = "value" coverts to {myprop: {somedepth: {key: value}}}
*/
let schema = obj;
const pList = path.split('.');
const len = pList.length;
for(var i = 0; i < len-1; i++) {
var elem = pList[i];
if( !schema[elem] ) schema[elem] = {}
schema = schema[elem];
}

schema[pList[len-1]] = value;

return schema
}


function displaySuccess() {

console.log(chalk.green("Successfully submitted Application update"))

}




export function convertOptionsToApiBody(options: any) {
let body: Record<string, any> = {
application: {}
}
console.log({ options })
if (options.redirectUrl) {
if (!body?.application?.oauthConfiguration) body.application.oauthConfiguration = {}
body.application.oauthConfiguration.authorizedRedirectURLs = [options.redirectUrl]
}

console.log(body)
return body
}

function splitProp(prop: string) {
const [key,value] = prop.split("=")
return {key, value}
}

const action = async function (options: Record<string, any>): Promise<void> {
const {
host = 'http://localhost:9011',
key,
id
} = options
const httpClient = new HTTPClient(host, key);

try {

if (options?.example) {
console.log(chalk.yellow("Generating example file in current directory"))
writeFileSync('./application.example.json', JSON.stringify(exampleApplicationBody, null, 2))
console.log(chalk.green(`File created at ${path.resolve('./application.example.json')}`))
return
}

if (options?.prop) {
let data = { application: {}}
const splitprops = options.prop.map((prop:string) => splitProp(prop))
splitprops.forEach((prop:any) => setNestedProps(data.application, prop.key, prop.value))
const response = await httpClient.executeRequest('PATCH', `/api/application/${id}`, data)
return
}

if (options?.data) {
const data = await getData(options.data)
await httpClient.executeRequest('PATCH', `/api/application/${id}`, { application: data })
displaySuccess()
return
}

const apiBody = convertOptionsToApiBody(options)
await httpClient.executeRequest('PATCH', `/api/application/${id}`, apiBody)
return

} catch (e) {
console.log(e)
}

}
export const appUpdate = new Command()
.command('application:update')
.requiredOption('-i, --id <id>', "The FusionAuth Application ID to update")
.option('-d, --data <file>', "Apply changes from a named file of JSON that matches the API body for an application update (ignores other flags)")
.option('--redirect-url <redirectUrl>', 'Oauth2.0 Authorized URL')
.option('-p, --prop <prop...>', 'Updates a single property from the application --prop name="My New Name" or --prop oauthConfiguration.authorizedOriginURLs="http://localhost:9011" ')
.option('--example', "Generate an example JSON document showing much of what can be updated via application:update")
.addOption(hostOption)
.addOption(apiKeyOption)
.description('Updates an application with data provided via a file, a property, or a command flag.')
.action(action)
1 change: 1 addition & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './application-update/index.js'
export * from './check-common-config.js';
export * from './email-create.js';
export * from './email-download.js';
Expand Down
197 changes: 196 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,4 +324,199 @@ async function updateGlobalConfig(propertiesToAdd: PropertyToAdd | PropertyToAdd
}

fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2))
}
}

export const exampleApplicationBody = {
"accessControlConfiguration": {},
"active": true,
"authenticationTokenConfiguration": {
"enabled": false
},
"data": {},
"emailConfiguration": {},
"externalIdentifierConfiguration": {},
"formConfiguration": {
"adminRegistrationFormId": "UUID",
"selfServiceFormConfiguration": {
"requireCurrentPasswordOnPasswordChange": false
}
},
"id": "UUID",
"insertInstant": 1234,
"jwtConfiguration": {
"accessTokenKeyId": "UUID",
"enabled": true,
"idTokenKeyId": "e73fe48a-1527-43cf-9b66-9eaa4c44d909",
"refreshTokenExpirationPolicy": "Fixed",
"refreshTokenOneTimeUseConfiguration": {
"gracePeriodInSeconds": 0
},
"refreshTokenSlidingWindowConfiguration": {
"maximumTimeToLiveInMinutes": 43200
},
"refreshTokenTimeToLiveInMinutes": 43200,
"refreshTokenUsagePolicy": "Reusable",
"timeToLiveInSeconds": 3600
},
"lambdaConfiguration": {},
"lastUpdateInstant": 1789396293195,
"loginConfiguration": {
"allowTokenRefresh": false,
"generateRefreshTokens": false,
"requireAuthentication": true
},
"multiFactorConfiguration": {
"email": {},
"sms": {},
"voice": {}
},
"name": "Name string",
"oauthConfiguration": {
"authorizedOriginURLs": [
"http://localhost:3000"
],
"authorizedRedirectURLs": [
"http://localhost:1002"
],
"authorizedResourceUris": [
"http://localhost:3000"
],
"authorizedURLValidationPolicy": "ExactMatch",
"clientAuthenticationPolicy": "NotRequiredWhenUsingPKCE",
"clientId": "UUID",
"clientSecret": "super-secret-secret-that-should-be-regenerated-for-production",
"consentMode": "AlwaysPrompt",
"debug": true,
"enabledGrants": [
"authorization_code",
"refresh_token"
],
"generateRefreshTokens": true,
"logoutBehavior": "AllApplications",
"logoutURL": "http://localhost:3000",
"proofKeyForCodeExchangePolicy": "Required",
"providedScopePolicy": {
"address": {
"enabled": true,
"required": false
},
"email": {
"enabled": true,
"required": false
},
"phone": {
"enabled": true,
"required": false
},
"profile": {
"enabled": true,
"required": false
}
},
"relationship": "FirstParty",
"requireClientAuthentication": true,
"requireRegistration": true,
"scopeHandlingPolicy": "Strict",
"unknownScopePolicy": "Reject"
},
"passwordlessConfiguration": {
"emailLoginStrategy": "ClickableLink",
"enabled": false,
"phoneLoginStrategy": "FormField"
},
"phoneConfiguration": {},
"registrationConfiguration": {
"birthDate": {
"enabled": false,
"required": false
},
"completeRegistration": false,
"confirmPassword": false,
"enabled": true,
"firstName": {
"enabled": false,
"required": false
},
"fullName": {
"enabled": false,
"required": false
},
"lastName": {
"enabled": false,
"required": false
},
"loginIdType": "email",
"middleName": {
"enabled": false,
"required": false
},
"mobilePhone": {
"enabled": false,
"required": false
},
"preferredLanguages": {
"enabled": false,
"required": false
},
"type": "basic"
},
"registrationDeletePolicy": {
"unverified": {
"enabled": false,
"numberOfDaysToRetain": 120
}
},
"roles": [],
"samlv2Configuration": {
"assertionEncryptionConfiguration": {
"digestAlgorithm": "SHA256",
"enabled": false,
"encryptionAlgorithm": "AES256GCM",
"keyLocation": "Child",
"keyTransportAlgorithm": "RSA_OAEP",
"maskGenerationFunction": "MGF1_SHA1"
},
"authorizedRedirectURLs": [],
"debug": false,
"enabled": false,
"initiatedLogin": {
"enabled": false,
"nameIdFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
},
"loginHintConfiguration": {
"enabled": true,
"parameterName": "login_hint"
},
"logout": {
"behavior": "AllParticipants",
"requireSignedRequests": false,
"singleLogout": {
"enabled": false,
"xmlSignatureC14nMethod": "exclusive_with_comments"
},
"xmlSignatureC14nMethod": "exclusive_with_comments"
},
"requireSignedRequests": false,
"xmlSignatureC14nMethod": "exclusive_with_comments",
"xmlSignatureLocation": "Assertion"
},
"scopes": [],
"state": "Active",
"tenantId": "d7d09513-a3f5-401c-9685-34ab6c552453",
"universalConfiguration": {
"universal": false
},
"unverified": {
"behavior": "Allow"
},
"verifyRegistration": false,
"webAuthnConfiguration": {
"bootstrapWorkflow": {
"enabled": false
},
"enabled": false,
"reauthenticationWorkflow": {
"enabled": false
}
}
}