sfdx-hardis
Version:
Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards
209 lines (202 loc) • 11.7 kB
JavaScript
/* jscpd:ignore-start */
import { SfCommand, Flags, requiredOrgFlagWithDeprecations } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import c from 'chalk';
import { generateReports, isCI, uxLog, uxLogTable } from '../../../../common/utils/index.js';
import { promptProfiles } from '../../../../common/utils/orgUtils.js';
//import { executeApex } from "../../../../common/utils/deployUtils.js";
import { prompts } from '../../../../common/utils/prompts.js';
import { soqlQuery, bulkQuery, bulkUpdate } from '../../../../common/utils/apiUtils.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('sfdx-hardis', 'org');
export default class OrgUnfreezeUser extends SfCommand {
static title = 'Unfreeze user logins';
static description = `
## Command Behavior
**Unfreezes Salesforce user logins, restoring access for selected users.**
This command allows administrators to unfreeze Salesforce user logins, reactivating their access to the Salesforce org. This is the counterpart to the \`freeze\` command and is used to restore access after a temporary suspension.
Key functionalities:
- **User Selection:** You can select users to unfreeze based on their assigned profiles.
- \`--includeprofiles\`: Unfreeze users belonging to a comma-separated list of specified profiles.
- \`--excludeprofiles\`: Unfreeze users belonging to all profiles *except* those specified in a comma-separated list.
- If no profile flags are provided, an interactive menu will allow you to select profiles.
- **Interactive Confirmation:** In non-CI environments, it prompts for confirmation before unfreezing the selected users.
- **Bulk Unfreezing:** Efficiently unfreezes multiple user logins using Salesforce's Bulk API.
- **Reporting:** Generates CSV and XLSX reports of the users that are about to be unfrozen.
<details markdown="1">
<summary>Technical explanations</summary>
The command's technical implementation involves:
- **SOQL Queries (Bulk API):** It executes SOQL queries against the \`User\` and \`Profile\` objects to identify active users based on the provided profile filters. It then queries the \`UserLogin\` object to find frozen login sessions for these users.
- **Interactive Prompts:** Uses the \`prompts\` library to guide the user through profile selection and to confirm the unfreezing operation.
- **Bulk Update:** It constructs an array of \`UserLogin\` records with their \`Id\` and \`IsFrozen\` set to \`false\`, then uses \`bulkUpdate\` to perform the mass update operation on the Salesforce org.
- **Reporting:** It uses \`generateReports\` to create CSV and XLSX files containing details of the users to be unfrozen.
- **Logging:** Provides clear messages about the number of users found and the success of the unfreezing process.
</details>
`;
static examples = [
`$ sf hardis:org:user:unfreeze`,
`$ sf hardis:org:user:unfreeze --target-org my-user@myorg.com`,
`$ sf hardis:org:user:unfreeze --includeprofiles 'Standard'`,
`$ sf hardis:org:user:unfreeze --excludeprofiles 'System Administrator,Some Other Profile'`,
];
// public static args = [{name: 'file'}];
static flags = {
// flag with a value (-n, --name=VALUE)
name: Flags.string({
char: 'n',
description: messages.getMessage('nameFilter'),
}),
includeprofiles: Flags.string({
char: 'p',
description: 'List of profiles that you want to unfreeze, separated by commas',
}),
excludeprofiles: Flags.string({
char: 'e',
description: 'List of profiles that you want to NOT unfreeze, separated by commas',
}),
maxuserdisplay: Flags.integer({
char: 'm',
default: 100,
description: 'Maximum users to display in logs',
}),
debug: Flags.boolean({
char: 'd',
default: false,
description: messages.getMessage('debugMode'),
}),
websocket: Flags.string({
description: messages.getMessage('websocket'),
}),
skipauth: Flags.boolean({
description: 'Skip authentication check when a default username is required',
}),
'target-org': requiredOrgFlagWithDeprecations,
};
// Set this to true if your command requires a project workspace; 'requiresProject' is false by default
static requiresProject = false;
maxUsersDisplay = 100;
debugMode = false;
/* jscpd:ignore-end */
async run() {
const { flags } = await this.parse(OrgUnfreezeUser);
const includeProfileNames = flags.includeprofiles ? flags.includeprofiles.split(',') : [];
const excludeProfileNames = flags.excludeprofiles ? flags.excludeprofiles.split(',') : [];
this.maxUsersDisplay = flags.maxuserdisplay || 100;
this.debugMode = flags.debug || false;
const conn = flags['target-org'].getConnection();
// Select profiles that we want users to be unfrozen
let profileIds = [];
let profileNames = [];
if (includeProfileNames.length === 0 && excludeProfileNames.length === 0) {
// Manual user selection
const profilesRes = await promptProfiles(conn, {
multiselect: true,
message: 'Please select profiles that you do you want to unfreeze users that are assigned to them ?',
returnField: 'record',
});
profileIds = profilesRes.map((profile) => profile.Id);
profileNames = profilesRes.map((profile) => {
return [profile.Id, profile.Name];
});
}
else if (includeProfileNames.length > 0) {
// Use includeprofiles argument
const profilesConstraintIn = includeProfileNames.map((profileName) => `'${profileName}'`).join(',');
const profilesQuery = `SELECT Id,Name FROM Profile WHERE Name IN (${profilesConstraintIn})`;
const profilesQueryRes = await soqlQuery(profilesQuery, conn);
if (this.debugMode) {
uxLog("log", this, c.grey(`Query result:\n${JSON.stringify(profilesQueryRes, null, 2)}`));
}
profileIds = profilesQueryRes.records.map((profile) => profile.Id);
profileNames = profilesQueryRes.records.map((profile) => {
return [profile.Id, profile.Name];
});
}
else if (excludeProfileNames.length > 0) {
// Use excludeprofiles argument
const profilesConstraintIn = excludeProfileNames.map((profileName) => `'${profileName}'`).join(',');
const profilesQuery = `SELECT Id,Name FROM Profile WHERE Name NOT IN (${profilesConstraintIn})`;
const profilesQueryRes = await soqlQuery(profilesQuery, conn);
if (this.debugMode) {
uxLog("log", this, c.grey(`Query result:\n${JSON.stringify(profilesQueryRes, null, 2)}`));
}
profileIds = profilesQueryRes.records.map((profile) => profile.Id);
profileNames = profilesQueryRes.records.map((profile) => {
return [profile.Id, profile.Name];
});
}
// List profiles that must be unfrozen
const profileIdsStr = profileIds.map((profileId) => `'${profileId}'`).join(',');
// Query users that we want to unfreeze
uxLog("action", this, c.cyan(`Querying User records matching ${c.bold(profileIds.length)} profiles...`));
const userQuery = `SELECT Id,Name,Username,ProfileId FROM User WHERE ProfileId IN (${profileIdsStr}) and IsActive=true`;
const userQueryRes = await bulkQuery(userQuery, conn);
const usersToUnfreeze = userQueryRes.records;
const userIdsStr = usersToUnfreeze.map((user) => `'${user.Id}'`).join(',');
// Check empty result
if (usersToUnfreeze.length === 0) {
const outputString = `No matching user records found with defined profile constraints`;
uxLog("warning", this, c.yellow(outputString));
return { outputString };
}
// Query related UserLogin records
uxLog("action", this, c.cyan(`Querying UserLogin records matching ${c.bold(usersToUnfreeze.length)} users...`));
const userLoginQuery = `SELECT Id,UserId,IsFrozen FROM UserLogin WHERE UserId IN (${userIdsStr}) and IsFrozen=true`;
const userLoginQueryRes = await bulkQuery(userLoginQuery, conn);
const userLoginsToUnfreeze = userLoginQueryRes.records;
// Display list of users to unfreeze
const usersToUnfreezeDisplay = userLoginsToUnfreeze.map((userLogin) => {
const matchingUser = usersToUnfreeze.filter((user) => user.Id === userLogin.UserId)[0];
return {
Username: matchingUser.Username,
Name: matchingUser.Name,
Profile: profileNames.filter((profile) => profile[0] === matchingUser.ProfileId)[1],
};
});
uxLog("action", this, c.cyan(`List of users to unfreeze (${userLoginsToUnfreeze.length}):`));
uxLogTable(this, this.debugMode ? usersToUnfreezeDisplay : usersToUnfreezeDisplay.slice(0, this.maxUsersDisplay));
if (!this.debugMode && usersToUnfreezeDisplay.length > this.maxUsersDisplay) {
uxLog("warning", this, c.yellow(c.italic(`(list truncated to the first ${this.maxUsersDisplay} users)`)));
}
// Generate csv + xls of users about to be unfrozen
await generateReports(usersToUnfreezeDisplay, ['Username', 'Name', 'Profile'], this, {
logFileName: 'users-to-unfreeze',
logLabel: 'Extract of users to unfreeze',
});
// Request configuration from user
if (!isCI) {
const confirmunfreeze = await prompts({
type: 'confirm',
name: 'value',
initial: true,
message: c.cyanBright(`Are you sure you want to unfreeze these ${c.bold(userLoginsToUnfreeze.length)} users in org ${c.green(flags['target-org'].getUsername())} ?`),
description: 'Confirm unfreezing selected users, which will reactivate their accounts in the Salesforce org',
});
if (confirmunfreeze.value !== true) {
const outputString = 'Script cancelled by user.';
uxLog("warning", this, c.yellow(outputString));
return { outputString };
}
}
// Process UserLogin freezing
const userLoginsFrozen = userLoginsToUnfreeze.map((userLogin) => {
return { Id: userLogin.Id, IsFrozen: false };
});
const bulkUpdateRes = await bulkUpdate('UserLogin', 'update', userLoginsFrozen, conn);
const unfreezeSuccessNb = bulkUpdateRes.successfulResults.length;
const unfreezeErrorsNb = bulkUpdateRes.failedResults.length;
if (unfreezeErrorsNb > 0) {
uxLog("warning", this, c.yellow(`Warning: ${c.red(c.bold(unfreezeErrorsNb))} users has not been unfrozen (bulk API errors)`));
}
// Build results summary
uxLog("success", this, c.green(`${c.bold(unfreezeSuccessNb)} users has been be unfrozen.`));
// Return an object to be displayed with --json
return {
orgId: flags['target-org'].getOrgId(),
unfreezeSuccess: unfreezeSuccessNb,
unfreezeErrors: unfreezeErrorsNb,
outputString: `${unfreezeSuccessNb} users has been be unfrozen`,
};
}
}
//# sourceMappingURL=unfreeze.js.map